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