前後端分離+springboot+springsecurity單點登錄sso實現(csrf過濾器post驗證)

springsecurity的單點登錄實現起來很容易,但是對csrf的過濾器攔截卡殼了三天,現在對這個測試Demo內容整理,希望幫助到遇到同樣問題的同學們!

現在開始講解:

一共三個項目,認證服務器A、第三方平臺B、第三方平臺C。下面分別進行說明

一、認證服務器A

先用maven構建好一個基本項目,然後進行開發;

目錄結構如下

pom引用主要加入以下四個依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>

編寫安全配置服務器

import com.security.sso.authorization.csrfHeader.CsrfHeaderFilter;
import com.security.sso.baseUtils.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.csrf.CsrfFilter;

@Configuration
public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {

	private final static Logger logger = LoggerFactory.getLogger(SsoSecurityConfig.class);

	@Autowired
	private UserDetailsService userDetailsService;

	@Autowired
	private PasswordEncoder passwordEncoder;

	@Autowired
	private SecurityProperties securityProperties;

	@Autowired
	protected AuthenticationFailureHandler cstAuthenticationFailureHandler;

	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception {
		logger.info("用自己定義的usersevices來驗證用戶");
		auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
	}


	@Override
	protected void configure(HttpSecurity http) throws Exception {
		//把登錄方式改成表單登錄的形式
		logger.info("-----定義表單登錄方式+自定義成功跳轉方法+自定義登錄頁面----");
		/**/
		http    //.csrf().disable()先禁用跨站訪問功能
				.formLogin()//表單登錄
				.loginPage("/authentication/require")//自定義登錄跳轉方法
				.loginProcessingUrl("/authentication/form")// 提交登錄表單地址(與登錄頁中提交的地址一致,就可以提交到登錄驗證服務MyUserDetailsService 中)
				//.successHandler(cstAuthenticationSuccessHandler)//成功後跳轉自定義方法
				.failureHandler(cstAuthenticationFailureHandler)//失敗後跳轉自定義方法
				.and()
				.httpBasic()
				.and()
				.authorizeRequests()
				.antMatchers("/authentication/require",securityProperties.getBrowser().getSignInPage()).permitAll()//這個頁面不需要身份認證,其他都需要
				.anyRequest()//任何請求
				.authenticated()//都需要身份認證
				.and()
				.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);//把CSRFtoken設定到cookie
		/**/
		//http.formLogin().and().authorizeRequests().anyRequest().authenticated();
	}
}

值得注意的是最後一行

.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);//把CSRFtoken設定到cookie

因爲項目採取前後端分離的開發模式,導致session功能失效,在springsecurity4以後默認開啓csrf驗證,來對應csrf的攻擊方式,關於什麼是csrf可以通過百度瞭解,springsecurity配置了相關過濾器,攔截post請求,爲了讓post請求可以通過驗證,我自定義了一個專門爲了cookie生成csrf_token傳到前臺的過濾器,在提交post請求的時候,利用js獲取了cookie中從後臺生成的csrf_token的值,作爲參數傳遞到請求中,有了這個參數,就可以通過springsecurity的csrf過濾器的驗證。這種情況後續再做詳細說明,畢竟卡殼兩天,身心俱疲。

配置認證服務器

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

//認證服務器
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

	private final static Logger logger = LoggerFactory.getLogger(SsoAuthorizationServerConfig.class);

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		logger.info("創建兩個客戶端,爲這兩個客戶端發送授權,或者通過配置文件配置");
		clients.inMemory()
				.withClient("appa")
				.secret("appa_ret")
				.authorizedGrantTypes("authorization_code", "refresh_token")
				.scopes("all")
				.and()
				.withClient("appb")
				.secret("appb_ret")
				.authorizedGrantTypes("authorization_code", "refresh_token")
				.scopes("all");
	}
	/** JWT令牌配置有關的兩個 bean **/
	@Bean
	public TokenStore jwtTokenStore(){
		return new JwtTokenStore(jwtAccessTokenConverter());
	}

	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter(){
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setSigningKey("ssodemo");//tokenKey
		logger.info("jwt的祕鑰是:ssodemo");
		return converter;
	}

	//生成令牌
	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
	}


	//安全配置
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		//要訪問授權服務器的tokenKey(簽名祕鑰)時,要經過身份認證
		//默認祕鑰是無法訪問的,這樣設置後,只要經過身份認證後,就可以拿到祕鑰
		security.tokenKeyAccess("isAuthenticated()");
	}

我在認證服務器定義了兩臺第三方服務器,就是文章開始所說的B 和 C 配置了他們的名稱和密碼和權限。

關於生成JWT令牌的代碼部分,重點關注密鑰的設置,因爲第三方服務器會通過拿去密鑰來解析令牌的內容,所以密鑰是很重要的,一定要注意密鑰的安全性。

最關鍵的就是這以上兩個類,如果是自定義登錄頁面的話,還有一些其他配置,接着說

跳轉登錄Controller
@RestController
public class JumpToLoginPageController {

	private Logger logger = LoggerFactory.getLogger(getClass());

	private RequestCache requestCache = new HttpSessionRequestCache();

	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

	@Autowired
	private SecurityProperties securityProperties;

	@RequestMapping("/authentication/require")
	@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
	public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {SavedRequest savedRequest = requestCache.getRequest(request, response);
		redirectStrategy.sendRedirect(request,response, securityProperties.getBrowser().getSignInPage());
		return new SimpleResponse("訪問的服務需要身份認證,請引導用戶到登錄頁",securityProperties.getBrowser().getSignInPage());
	}

}
/**登錄失敗後的跳轉*/
@Component("cstAuthenticationFailureHandler")   //spring security 默認處理器
public class CstAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { //implements AuthenticationFailureHandler

	private final static Logger logger = LoggerFactory.getLogger(CstAuthenticationFailureHandler.class);

	@Autowired
	private ObjectMapper objectMapper;

	@Autowired
	private SecurityProperties securityProperties;

	//登錄失敗有很多原因,對應不同的異常
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
	                                    AuthenticationException e) throws IOException, ServletException {
		logger.info("登錄失敗");

		//如果自定義設定返回的是JSON格式內容,就把內容返回到前臺即可
		if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
			response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//服務器內部異常
			response.setContentType("application/json;charset=UTF-8");
			response.getWriter().write(objectMapper.writeValueAsString(e));
		}else{
			super.onAuthenticationFailure(request,response,e);
		}
	}
}

用戶service,用於登錄用戶查詢和權限查詢必須實現接口 UserDetailsService, SocialUserDetailsService 的方法

loadUserByUsername
import com.security.sso.userPart.entity.*;
import com.security.sso.userPart.service.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;

@Component
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {

	private final static Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);

	/********注入自定義的用戶service********/
	@Autowired
	private final SysUserService sysUserService;
	@Autowired
	private SysPermissionService sysPermissionService;
	@Autowired
	private SysRoleService sysRoleService;
	@Autowired
	private SysRoleUserService sysRoleUserService;
	@Autowired
	private SysPermissionRoleService sysPermissionRoleService;

	@Autowired
	MyUserDetailsService(SysUserService sysUserService){
		this.sysUserService = sysUserService;
	}

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		//表單登錄
		Sys_User sysUser = sysUserService.selectByUsername(username);
		if (sysUser == null) {
			throw new UsernameNotFoundException("用戶不存在!");
			//返回方式二:返回帶失敗原因的數據對象
			//return new User(username, userEntity.getPassword(), true,true,true,true,
			//		AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
		}else{
			logger.info("用戶存在,用戶:" + username);
			//把用戶的角色賦給該用戶當作該用戶的權限
			List<Sys_Role_User> sruList = sysRoleUserService.selectByUser_id(sysUser.getId());
			List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
			for(Sys_Role_User ru : sruList){
				Sys_Role role = sysRoleService.selectRoleById(ru.getSys_role_id());
				logger.info(username+"-->role:"+role.getName());
				GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getName());
				//1:此處將權限信息添加到 GrantedAuthority 對象中,在後面進行全權限驗證時會使用GrantedAuthority 對象。
				grantedAuthorities.add(grantedAuthority);
			}
			return new User(sysUser.getUsername(), sysUser.getPassword(), grantedAuthorities);
		}
	}


	@Override
	public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
		return null;
	}
}

再UserDetailsService裏登錄成功後查詢出用戶的權限(角色)把這些權限放到該用戶的授權信息中,返回

GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getName()); grantedAuthorities.add(grantedAuthority);

用戶登錄後要自動彈出確認授權的頁面,需要用戶自己選擇是否要確認授權。爲了給用戶更好的體驗,重寫了確認授權的頁面彈出方式,讓該確認頁面改爲自動提交的方式

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.util.Iterator;
import java.util.Map;

/**拷貝自源碼**/
/**org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint**/
/**需要授權的時候本來源碼要彈出確認授權表單頁面,現在改造成**/
@RestController
@SessionAttributes("authorizationRequest")
public class SsoApprovalEndpoint {

	private static String CSRF = "<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}' />";
	private static String DENIAL = "<form id='denialForm' name='denialForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input name='deny' value='Deny' type='submit'/></label></form>";
	private static String TEMPLATE = "<html>" +
			"<body>" +
			"<div style='display:none;'>" +  //增加DIV 把HTML內容隱藏
			"<h1>OAuth Approval</h1><p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p><form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>%denial%" +
			"</div>" +
			"<script>document.getElementById('confirmationForm').submit()</script>" + //白頁面--自動提交表單
			"</body>" +
			"</html>";
	private static String SCOPE = "<li><div class='form-group'>%scope%: <input type='radio' name='%key%' value='true'%approved%>Approve</input> <input type='radio' name='%key%' value='false'%denied%>Deny</input></div></li>";

	public SsoApprovalEndpoint() {
	}

	@RequestMapping({"/oauth/confirm_access"})
	public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
		String template = this.createTemplate(model, request);
		if (request.getAttribute("_csrf") != null) {
			model.put("_csrf", request.getAttribute("_csrf"));
		}

		return new ModelAndView(new SsoSpelView(template), model);
	}

	protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
		String template = TEMPLATE;
		if (!model.containsKey("scopes") && request.getAttribute("scopes") == null) {
			template = template.replace("%scopes%", "").replace("%denial%", DENIAL);
		} else {
			template = template.replace("%scopes%", this.createScopes(model, request)).replace("%denial%", "");
		}

		if (!model.containsKey("_csrf") && request.getAttribute("_csrf") == null) {
			template = template.replace("%csrf%", "");
		} else {
			template = template.replace("%csrf%", CSRF);
		}

		return template;
	}

	private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) {
		StringBuilder builder = new StringBuilder("<ul>");
		Map<String, String> scopes = (Map)((Map)(model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes")));
		Iterator var5 = scopes.keySet().iterator();

		while(var5.hasNext()) {
			String scope = (String)var5.next();
			String approved = "true".equals(scopes.get(scope)) ? " checked" : "";
			String denied = !"true".equals(scopes.get(scope)) ? " checked" : "";
			String value = SCOPE.replace("%scope%", scope).replace("%key%", scope).replace("%approved%", approved).replace("%denied%", denied);
			builder.append(value);
		}

		builder.append("</ul>");
		return builder.toString();
	}

}
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

/**拷貝自源碼**/
/**org.springframework.security.oauth2.provider.endpoint.SpelView**/
public class SsoSpelView implements View {

	private final String template;
	private final String prefix;
	private final SpelExpressionParser parser = new SpelExpressionParser();
	private final StandardEvaluationContext context = new StandardEvaluationContext();
	private PlaceholderResolver resolver;

	public SsoSpelView(String template) {
		this.template = template;
		this.prefix = (new RandomValueStringGenerator()).generate() + "{";
		this.context.addPropertyAccessor(new MapAccessor());
		this.resolver = new PlaceholderResolver() {
			public String resolvePlaceholder(String name) {
				Expression expression = SsoSpelView.this.parser.parseExpression(name);
				Object value = expression.getValue(SsoSpelView.this.context);
				return value == null ? null : value.toString();
			}
		};
	}

	public String getContentType() {
		return "text/html";
	}

	public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
		Map<String, Object> map = new HashMap(model);
		String path = ServletUriComponentsBuilder.fromContextPath(request).build().getPath();
		map.put("path", path == null ? "" : path);
		this.context.setRootObject(map);
		String maskedTemplate = this.template.replace("${", this.prefix);
		PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(this.prefix, "}");
		String result = helper.replacePlaceholders(maskedTemplate, this.resolver);
		result = result.replace(this.prefix, "${");
		response.setContentType(this.getContentType());
		response.getWriter().append(result);
	}

}

現在重點說說CSRF的攔截器的修改,找到csrf攔截器看看源碼是怎麼對請求進行處理的

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);

解釋:先從session中拿到csrfToken的值,如果是第一次進入該方法,csrfToken是不存在的,拿到的是null
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        boolean missingToken = csrfToken == null;
解釋:若果拿到爲null就根據本次請求的地址生成Token,並把該Token存儲到session中
        if (missingToken) {
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
解釋:把csrfToken放到request中
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
解釋:如果該請求時非post請求,直接進入後續流程
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
        } else {
解釋:如果是POST請求,重request中獲取csrfToken傳給actualToken
            String actualToken = request.getHeader(csrfToken.getHeaderName());
            if (actualToken == null) {
                actualToken = request.getParameter(csrfToken.getParameterName());
            }
解釋:驗證從request中傳過來的csrfToken和本次方法中的csrfToken是不是一致,不一致的話,拋出異常信息
            if (!csrfToken.getToken().equals(actualToken)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
                }

                if (missingToken) {
                    this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
                } else {
                    this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
                }

            } else {
                filterChain.doFilter(request, response);
            }
        }
    }

因爲是前後端分離的項目,沒有session的概念,也無法從session中獲取信息,參考了官網的解決辦法,把csrfToken放到cookie中,於是增加一個往cookie中加csrfToken的過濾器,該過濾器把csrfToken的信息放到cookie,並把cookie傳到前臺;

import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CsrfHeaderFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

		CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
		if (csrf != null) {
			Cookie cookie = WebUtils.getCookie(request, "CSRF-TOKEN");
			String token = csrf.getToken();
			if (cookie==null || token!=null && !token.equals(cookie.getValue())) {
				cookie = new Cookie("CSRF-TOKEN", token);
				cookie.setPath("/");
				response.addCookie(cookie);
			}
		}
		filterChain.doFilter(request, response);
	}
}

這樣把csrfToken的內容就可以通過前臺JS獲取,通過請求發送到後臺,這樣就會通過CsrfFilter的驗證,從而POST請求可以正常執行,這就有了前邊的把該過濾器註冊到過濾器鏈中

.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);//把CSRFtoken設定到cookie

下面說前臺登錄頁面的寫法

<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>登錄</title>
    <style>
        body{ text-align:center}
        .div{ margin:0 auto; width:400px; height:100px; border:1px solid #F00}
    </style>
</head>

<body style="background-color: #f1f1f1; padding-bottom: 0">
<form action="/server/authentication/form" method="post">
    <div class="div">
        <table>
            <tr>
                <td>用戶名:</td>
                <td>
                    <input class="cookie" type="hidden" name="_csrf">
                    <input class="username" type="text" name="username">
                </td>
            </tr>
            <tr>
                <td>密碼:</td>
                <td><input class="password" type="password" name="password"></td>
            </tr>
            <tr>
                <td colspan="2"><button type="submit">登錄</button></td>
            </tr>
        </table>
    </div>
</form>
</body>
<script type="text/javascript">
    var username = document.getElementsByClassName('usesrname')[0]
    var password = document.getElementsByClassName('password')[0]
    var cookie = getCookie('CSRF-TOKEN')
    console.log(cookie)
    document.getElementsByClassName('cookie')[0].value = cookie

    function getCookie(name)
    {
        var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");

        if(arr=document.cookie.match(reg))

            return unescape(arr[2]);
        else
            return null;
    }
    //格式化參數
    function formatParams(data) {
        var arr = [];
        for (var name in data) {
            arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
        }
        arr.push(("v=" + Math.random()).replace(".",""));
        return arr.join("&");
    }
</script>
</html>

可以看到前臺通過js從cookie 中拿到csrfToken的信息(_csrf),跟用戶名和密碼一起發送到後臺,這樣過濾器就可以拿到了csrfToken,從而通過驗證。

服務器設置要點講述完畢。

二、第三方平臺B

pom

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>

該項目結構

訪問資源之前必須先登錄到認證服務器中進行認證,所以配置文件中必須找到認證服務器的相關服務接口位置

server.port = 9002
server.context-path = /appa

#客戶端應用
security.oauth2.client.client-id = appa
security.oauth2.client.client-secret = appa_ret
security.basic.enabled = false
#認證服務器地址
security.oauth2.client.user-authorization-uri = http://127.0.0.1:9001/server/oauth/authorize
#向認證服務器請求令牌的地址
security.oauth2.client.access-token-uri = http://127.0.0.1:9001/server/oauth/token
#認證服務器拿回祕鑰
security.oauth2.resource.jwt.key-uri = http://127.0.0.1:9001/server/oauth/token_key

定義資源controller

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;

@EnableOAuth2Sso  //單點登錄註解
@RestController
@RequestMapping("/user")
public class UserController {

	private final static Logger logger = LoggerFactory.getLogger(UserController.class);

	//獲取當前人的認證信息
	@GetMapping("/me")
	public Object getCurrentUser(Authentication user, HttpServletRequest request){
		return user;
	}

	/*****************測試用戶訪問菜單權限*****************/
	@PostMapping("/menu_a")
	public String menu_a(){
		logger.info(" into '/user/menu_a' method ");
		return "this is menu_a";
	}

	@PostMapping("/menu_b")
	public String menu_b(){
		logger.info(" into '/user/menu_b' method ");
		return "this is menu_b";
	}

	@PostMapping("/menu_c")
	public String menu_c(){
		logger.info(" into '/user/menu_c' method ");
		return "this is menu_c";
	}
}

關鍵點是單點登錄註解   @EnableOAuth2Sso //單點登錄註解

下面還是說說關於csrfToken的設置,劃重點!!!

定義過濾器

import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CsrfHeaderFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		//把csrfToken設定到cookie中發送到前臺
		CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
		if (csrf != null) {
			//Cookie cookie = WebUtils.getCookie(request, "CSRF-TOKEN");
			String token = csrf.getToken();
			Cookie cookie = new Cookie("X-CSRF-TOKEN", token);
			cookie.setPath("/");
			response.addCookie(cookie);
		}
		filterChain.doFilter(request, response);
	}
}

註冊該過濾器

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CsrfFilterConfig {
	@Bean
	public FilterRegistrationBean filterRegist() {
		FilterRegistrationBean frBean = new FilterRegistrationBean();
		frBean.setFilter(new CsrfHeaderFilter());
		frBean.addUrlPatterns("/*");
		System.out.println("註冊CSRFCOOKIE過濾器");
		return frBean;
	}
}

跟認證服務器一樣訪問資源之前先把cookie中的csrfToken傳遞到前端,然後通過js把csrfToken傳遞到後臺從而通過驗證

csrfToken是跟隨該用戶這次登錄操作的,如果用戶退出登錄,或者瀏覽器清除了cookie,就需要重新獲取該Token了

編寫頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SSO Client appa</title>
</head>
<body>
    <h1>SSO Client appa ==》》 appb</h1>
    <a href="http://127.0.0.1:9003/appb/index.html">訪問 appb </a>
</body>
</html>

內容要點就以上

三、第三方平臺C

與平臺B類似,下面只貼一下不同點

server.port = 9003
server.context-path = /appb
#客戶端應用
security.oauth2.client.client-id = appb
security.oauth2.client.client-secret = appb_ret
security.basic.enabled = false
#認證服務器地址
security.oauth2.client.user-authorization-uri = http://127.0.0.1:9001/server/oauth/authorize
#向認證服務器請求令牌的地址
security.oauth2.client.access-token-uri = http://127.0.0.1:9001/server/oauth/token
#認證服務器拿回祕鑰
security.oauth2.resource.jwt.key-uri = http://127.0.0.1:9001/server/oauth/token_key

前端頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SSO Client appb</title>
</head>
<body>
    <h1>SSO Client appb ==》》 appa</h1>
    <a href="http://127.0.0.1:9002/appa/index.html">訪問 appa </a>
</body>
</html>

 

下面演示一下登錄的過程

啓動三個服務,先啓動認證服務,然後再啓動第三方服務

地址欄輸入要訪問的第三方服務資源  http://127.0.0.1:9002/appa/index.html 因爲所有資源都收到springsecurity的保護,所有地址會跳轉到認證服務器的登錄頁面

從這裏可以看到它是如何一步一步跳轉過來的

輸入用戶信息點擊登錄,登錄成功,跳轉回所要訪問的資源頁面

點擊鏈接就可以訪問第三方平臺C,而不用再登錄第三方平臺C,系統會自動進行登錄

這樣就可以以在兩個系統之間跳轉

下面來說一下訪問第三方服務的其他資源的操作,主要是csrfToken的獲取

我們知道只有當發起POST請求才必須通過csrfToken的驗證,如果發送的是GET請求,只要滿足權限要求就可以訪問,比如說訪問B平臺的 http://127.0.0.1:9002/appa/user/me 這個請求,就可以直接從地址欄中輸入地址發送GET請求獲取數據

重點看一下response Cookie 裏邊已經獲取到了csrfToken的值,如果我們要發送post請求到後臺,這個值就非常重要了,下面是發送post請求的過程,我們用restlet來發送post請求

在發送請求時帶上了從cookie中獲取的csrfToken的值,給_csrf參數賦值後一併發送到後臺,才能正確獲得請求的數據

如果不帶csrfToken就會報錯 403   表示理解你的請求但是不能處理

Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'

當時查了很多資料,走了彎路,因爲大部分資料都是講怎樣在吧.csrf().disable();禁用了,然後我在認證服務器那裏禁用了,但是方位第三方平臺還報,後來發現認證服務器只是禁用了自己,不管第三方平臺,然後我又在第三方平臺也禁用了,就是自己寫了一個SsoSecurityConfig 跟認證服務器一樣.csrf().disable();,結果第三方平臺不走單點登錄了,開始走自己的認證服務了,又趕緊把這個類刪除了,最後纔想出了自己寫過濾器的辦法,還是要看官方文檔啊,雖然官方文檔寫的不是前後端分離的項目,只是說明表單提交要加隱藏域把csrfToken加進去。只能自己又做了一些改動,到了現在這個樣子。

頁面測試

 

源碼下載:https://gitee.com/zhangchai/ssodemo.git

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章