springboot+springsecurity单点登录sso实现(csrf过滤器post验证)

springsecurity的单点登录实现起来很容易,但是对csrf的过滤器拦截卡壳了三天,现在对这个测试Demo内容整理,希望帮助到遇到同样问题的同学们!

现在开始讲解:

一共三个项目,认证服务器A、第三方平台B、第三方平台C。下面分别进行说明

一、认证服务器A

先用maven构建好一个基本项目,然后进行开发;

目录结构如下

springboot+springsecurity单点登录sso实现(csrf过滤器post验证)_第1张图片

pom引用主要加入以下四个依赖

        
            org.springframework.boot
            spring-boot-starter-security
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.security.oauth
            spring-security-oauth2
        
        
            org.springframework.security
            spring-security-jwt
        

编写安全配置服务器

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 sruList = sysRoleUserService.selectByUser_id(sysUser.getId());
			List 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 = "";
	private static String DENIAL = "
%csrf%
"; private static String TEMPLATE = "" + "" + "
" + //增加DIV 把HTML内容隐藏 "

OAuth Approval

Do you authorize '${authorizationRequest.clientId}' to access your protected resources?

%csrf%%scopes%
%denial%" + "
" + "" + //白页面--自动提交表单 "" + ""; private static String SCOPE = "
  • %scope%: Approve Deny
  • "; public SsoApprovalEndpoint() { } @RequestMapping({"/oauth/confirm_access"}) public ModelAndView getAccessConfirmation(Map 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 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 model, HttpServletRequest request) { StringBuilder builder = new StringBuilder("
      "); Map 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("
    "); 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 model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    		Map 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

    下面说前台登录页面的写法

    
    
        
        
        登录
        
    
    
    
    
    用户名:
    密码:

    可以看到前台通过js从cookie 中拿到csrfToken的信息(_csrf),跟用户名和密码一起发送到后台,这样过滤器就可以拿到了csrfToken,从而通过验证。

    服务器设置要点讲述完毕。

    二、第三方平台B

    pom

            
                org.springframework.boot
                spring-boot-starter-security
            
            
                org.springframework.boot
                spring-boot-starter-web
            
            
                org.springframework.security.oauth
                spring-security-oauth2
            
            
                org.springframework.security
                spring-security-jwt
            

    该项目结构

    springboot+springsecurity单点登录sso实现(csrf过滤器post验证)_第2张图片

    访问资源之前必须先登录到认证服务器中进行认证,所以配置文件中必须找到认证服务器的相关服务接口位置

    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了

    编写页面

    
    
    
        
        SSO Client appa
    
    
        

    SSO Client appa ==》》 appb

    访问 appb

    内容要点就以上

    三、第三方平台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

    前端页面

    
    
    
        
        SSO Client appb
    
    
        

    SSO Client appb ==》》 appa

    访问 appa

     

    下面演示一下登录的过程

    启动三个服务,先启动认证服务,然后再启动第三方服务

    地址栏输入要访问的第三方服务资源  http://127.0.0.1:9002/appa/index.html 因为所有资源都收到springsecurity的保护,所有地址会跳转到认证服务器的登录页面

    springboot+springsecurity单点登录sso实现(csrf过滤器post验证)_第3张图片

    从这里可以看到它是如何一步一步跳转过来的

    输入用户信息点击登录,登录成功,跳转回所要访问的资源页面

    springboot+springsecurity单点登录sso实现(csrf过滤器post验证)_第4张图片

    点击链接就可以访问第三方平台C,而不用再登录第三方平台C,系统会自动进行登录

    springboot+springsecurity单点登录sso实现(csrf过滤器post验证)_第5张图片

    这样就可以以在两个系统之间跳转

    下面来说一下访问第三方服务的其他资源的操作,主要是csrfToken的获取

    我们知道只有当发起POST请求才必须通过csrfToken的验证,如果发送的是GET请求,只要满足权限要求就可以访问,比如说访问B平台的 http://127.0.0.1:9002/appa/user/me 这个请求,就可以直接从地址栏中输入地址发送GET请求获取数据

    springboot+springsecurity单点登录sso实现(csrf过滤器post验证)_第6张图片

    重点看一下response Cookie 里边已经获取到了csrfToken的值,如果我们要发送post请求到后台,这个值就非常重要了,下面是发送post请求的过程,我们用restlet来发送post请求

    springboot+springsecurity单点登录sso实现(csrf过滤器post验证)_第7张图片

    在发送请求时带上了从cookie中获取的csrfToken的值,给_csrf参数赋值后一并发送到后台,才能正确获得请求的数据

    如果不带csrfToken就会报错 403   表示理解你的请求但是不能处理

    springboot+springsecurity单点登录sso实现(csrf过滤器post验证)_第8张图片

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

    当时查了很多资料,走了弯路,因为大部分资料都是讲怎样在吧.csrf().disable();禁用了,然后我在认证服务器那里禁用了,但是方位第三方平台还报,后来发现认证服务器只是禁用了自己,不管第三方平台,然后我又在第三方平台也禁用了,就是自己写了一个SsoSecurityConfig 跟认证服务器一样.csrf().disable();,结果第三方平台不走单点登录了,开始走自己的认证服务了,又赶紧把这个类删除了,最后才想出了自己写过滤器的办法,还是要看官方文档啊,虽然官方文档写的不是前后端分离的项目,只是说明表单提交要加隐藏域把csrfToken加进去。只能自己又做了一些改动,到了现在这个样子。

    页面测试springboot+springsecurity单点登录sso实现(csrf过滤器post验证)_第9张图片

     

    源码下载:https://gitee.com/zhangchai/ssodemo.git

     

     

     

    你可能感兴趣的:(springboot+springsecurity单点登录sso实现(csrf过滤器post验证))