springsecurity的单点登录实现起来很容易,但是对csrf的过滤器拦截卡壳了三天,现在对这个测试Demo内容整理,希望帮助到遇到同样问题的同学们!
现在开始讲解:
一共三个项目,认证服务器A、第三方平台B、第三方平台C。下面分别进行说明
一、认证服务器A
先用maven构建好一个基本项目,然后进行开发;
目录结构如下
pom引用主要加入以下四个依赖
编写安全配置服务器
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
List
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 = "