前后端分离+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

 

 

 

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