Spring Security流程
说白了,它就是一堆的拦截器。下面叙述以下其认证流程。
AbstractAuthenticationProcessingFilter
先看 AbstractAuthenticationProcessingFilter,其是UsernamePasswordAuthenticationFilter的父类。
在其Filter中调用了其子类的验证方法。
//核心方法
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//调用UsernamePasswordAuthenticationFilter的方法
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
//将其认证信息authenticationResult放入session中
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 认证成功调用方法
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
// 认证失败调用方法
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// 认证失败调用方法
unsuccessfulAuthentication(request, response, ex);
}
}
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter,核心拦截器,当有post请求进来时会进如该方法,用户名默认username,密码password
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//...
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//对其进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
//...
}
UsernamePasswordAuthenticationToken讲解:
该方法有两个构造方法,一个是已经认证成功的,另一个是没有认证成功的。是对表单提交过来的数据进行封装。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
...
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
...
}
AuthenticationManager:
认证接口;常用子类ProviderManager:提供相应的AuthenticationProvider来进行认证
//UsernamePasswordAuthenticationToke认证的返回,点击进入下面方法。
return this.getAuthenticationManager().authenticate(authRequest);
//AuthenticationProvider集合
private List<AuthenticationProvider> providers = Collections.emptyList();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
//遍历选择对应的AuthenticationProvider然后提供验证。,大致了解即可。
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
AuthenticationProvider
紧接着我们看AuthenticationProvider类,因为在上述中其提供验证。
AuthenticationProvider接口子类如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oAFXPM0S-1644829572031)(C:\Users\736263\AppData\Roaming\Typora\typora-user-images\image-20211122155740713.png)]
看其中的DaoAuthenticationProvider:
核心代码如下
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
请注意看该段代码
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
改代码属于接口UserDetailsService,该接口的实现类为下图,如果我们自己想要查询自己的数据库,只需要实现UserDetailsService重新写loadUserByUsername方法(下示)即可。
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k7zobfUu-1644829572033)(C:\Users\736263\AppData\Roaming\Typora\typora-user-images\image-20211122160314851.png)]
UserDetails
讲解:这个类是系统默认的用户“主体”
// 表示获取登录用户所有权限Collection<? extends GrantedAuthority> getAuthorities();// 表示获取密码String getPassword();// 表示获取用户名String getUsername();// 表示判断账户是否过期boolean isAccountNonExpired();// 表示判断账户是否被锁定boolean isAccountNonLocked();// 表示凭证{密码}是否过期boolean isCredentialsNonExpired();// 表示当前用户是否可用boolean isEnabled();
自定义过滤器:
过滤器链执行顺序(从上到下)
HeaderWriterFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterBasicAuthenticationFilter...FilterSecurityInterceptor
故如果想要添加权限授权,可以集成BasicAuthenticationFilter该类
eg:
public class TokenAuthFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenAuthFilter(AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) { super(authenticationManager); this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //获取当前认证成功用户权限信息 UsernamePasswordAuthenticationToken authRequest = getAuthentication(request); //判断如果有权限信息,放到权限上下文中 if(authRequest != null) { SecurityContextHolder.getContext().setAuthentication(authRequest); } chain.doFilter(request,response); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { //从header获取token String token = request.getHeader("token"); if(token != null) { //从token获取用户名 String username = tokenManager.getUserInfoFromToken(token); //从redis获取对应权限列表 List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username); Collection<GrantedAuthority> authority = new ArrayList<>(); for(String permissionValue : permissionValueList) { SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue); authority.add(auth); } return new UsernamePasswordAuthenticationToken(username,token,authority); } return null; }}
或者继承GenericFilterBean
public class BeforeLoginFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("在 UsernamePasswordAuthenticationFilter 前调用"); chain.doFilter(request, response); }}
之后在配置类中配置即可
//设置退出的地址和token,redis操作地址 @Override protected void configure(HttpSecurity http) throws Exception { ... http.addFilter();//添加这句代码即可 ... }
说明: HttpSecurity 有三个常用方法来配置:addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) 在 beforeFilter 之前添加 filter addFilterAfter(Filter filter, Class<? extends Filter> afterFilter) 在 afterFilter 之后添加 filteraddFilterAt(Filter filter, Class<? extends Filter> atFilter) 在 atFilter 相同位置添加 filter, 此 filter 不覆盖 filter 通过在不同 Filter 的 doFilter() 方法中加断点调试,可以判断哪个 filter 先执行,从而判断 filter 的执行顺序 。
Spring Security的session和token绑定原理
1.当你在登录的时候,会进入UsernamePasswordAuthenticationFilter,查看其父类AbstractAuthenticationProcessingFilter的源码。注意下面这一句。
如果第一次,则会将其认证信息与session绑定起来,并且只次一份。
//将其认证信息authenticationResult放入session中this.sessionStrategy.onAuthentication(authenticationResult, request, response);
即Authentication被securityContext封装,然后securityContextHolder是将ThreadLocal和securityContext绑定。
SecurityContextHolder.getContext()//如果有则返回securityContext,如果没有则会创建SecurityContextHolder.getContext().setAuthentication(authRequest);//会将其的认证信息覆盖
Spring Security整合JWT
当系统登录后重写successfulAuthentication方法,给浏览器返回一个token,
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; private AuthenticationManager authenticationManager; public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) { this.authenticationManager = authenticationManager; this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; this.setPostOnly(false); this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST")); } //1 获取表单提交用户名和密码 @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //获取表单提交数据 try { User user = new ObjectMapper().readValue(request.getInputStream(), User.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(), new ArrayList<>())); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(); } } //2 认证成功调用的方法 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //认证成功,得到认证成功之后用户信息 SecurityUser user = (SecurityUser)authResult.getPrincipal(); //根据用户名生成token String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername()); //把用户名称和用户权限列表放到redis redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList()); //返回token ResponseUtil.out(response, R.ok().data("token",token)); } //3 认证失败调用的方法 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { ResponseUtil.out(response, R.error()); }}
然后授权,重写SecurityContext的认证信息,授权。
public class TokenAuthFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenAuthFilter(AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) { super(authenticationManager); this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //获取当前认证成功用户限信息 UsernamePasswordAuthenticationToken authRequest = getAuthentication(request); //判断如果有权限信息,放到权限上下文中 if(authRequest != null) { SecurityContextHolder.getContext().setAuthentication(authRequest); } chain.doFilter(request,response); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { //从header获取token String token = request.getHeader("token"); if(token != null) { //从token获取用户名 String username = tokenManager.getUserInfoFromToken(token); //从redis获取对应权限列表 List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username); Collection<GrantedAuthority> authority = new ArrayList<>(); for(String permissionValue : permissionValueList) { SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue); authority.add(auth); } return new UsernamePasswordAuthenticationToken(username,token,authority); } return null; }}
生成token的工具类
public class TokenManager { //token有效时长 private long tokenEcpiration = 24*60*60*1000; //编码秘钥 private String tokenSignKey = "123456"; //1 使用jwt根据用户名生成token public String createToken(String username) { String token = Jwts.builder().setSubject(username) .setExpiration(new Date(System.currentTimeMillis()+tokenEcpiration)) .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact(); return token; } //2 根据token字符串得到用户信息 public String getUserInfoFromToken(String token) { String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject(); return userinfo; } //3 删除token public void removeToken(String token) { }}
spring security推荐使用 BCryptPasswordEncoder 对密码进行加密,只需将其加入spring中即可。
public static void main(String[] args) { // 创建密码解析器 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();// 对密码进行加密 String atguigu = bCryptPasswordEncoder.encode("123456");// 打印加密之后的数据 System.out.println("加密之后数据:\t"+atguigu);//判断原字符加密后和加密之前是否匹配 boolean result = bCryptPasswordEncoder.matches("123456", atguigu);// 打印比较结果 System.out.println("比较结果:\t"+result); }
Spring security配置类
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { private TokenManager tokenManager; private RedisTemplate redisTemplate; private DefaultPasswordEncoder defaultPasswordEncoder; private UserDetailsService userDetailsService; @Autowired public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder, TokenManager tokenManager, RedisTemplate redisTemplate) { this.userDetailsService = userDetailsService; this.defaultPasswordEncoder = defaultPasswordEncoder; this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; } /** * 配置设置 * @param http * @throws Exception */ //设置退出的地址和token,redis操作地址 @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问 .and().csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and().logout().logoutUrl("/admin/acl/index/logout")//退出路径 .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and() .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate)) .addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic(); } //调用userDetailsService和密码处理 @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder); } //不进行认证的路径,可以直接访问 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/api/**"); }}
开启注释:
@Secured
//判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。 使用注解先要开启注解功能!
@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled=true)@RequestMapping("testSecured")@ResponseBody@Secured({"ROLE_normal","ROLE_admin"})public String helloUser() {return "hello,user";}
@PreAuthorize
@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用
户的 roles/permissions 参数传到方法中。
先开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true)@RequestMapping("/preAuthorize")@ResponseBody//@PreAuthorize("hasRole('ROLE_管理员')")@PreAuthorize("hasAnyAuthority('menu:system')")public String preAuthorize(){ System.out.println("preAuthorize");return "preAuthorize";}
@PostAuthorize
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值
的权限.
先开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true) @RequestMapping("/testPostAuthorize")@ResponseBody@PostAuthorize("hasAnyAuthority('menu:system')")public String preAuthorize(){ System.out.println("test--PostAuthorize");return "PostAuthorize";}