欢迎关注方志朋的博客,回复”666“获面试宝典

来源:blog.csdn.net/zzzgd_666/article/details/96444829

前言

一般来说,我们用SpringSecurity默认的话是前后端整在一起的,比如thymeleaf或者Freemarker,SpringSecurity还自带login登录页,还让你配置登出页,错误页。

但是现在前后端分离才是正道,前后端分离的话,那就需要将返回的页面换成Json格式交给前端处理了

SpringSecurity默认的是采用Session来判断请求的用户是否登录的,但是不方便分布式的扩展,虽然SpringSecurity也支持采用SpringSession来管理分布式下的用户状态,不过现在分布式的还是无状态的Jwt比较主流。

所以下面说下怎么让SpringSecurity变成前后端分离,采用Jwt来做认证的

一、五个handler一个filter两个User

5个handler,分别是

  • 实现AuthenticationEntryPoint接口,当匿名请求需要登录的接口时,拦截处理

  • 实现AuthenticationSuccessHandler接口,当登录成功后,该处理类的方法被调用

  • 实现AuthenticationFailureHandler接口,当登录失败后,该处理类的方法被调用

  • 实现AccessDeniedHandler接口,当登录后,访问接口没有权限的时候,该处理类的方法被调用

  • 实现LogoutSuccessHandler接口,注销的时候调用

1.1 AuthenticationEntryPoint

匿名未登录的时候访问,遇到需要登录认证的时候被调用

/*** 匿名未登录的时候访问,需要登录的资源的调用类* @author zzzgd*/
@Component
public class CustomerAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {//设置response状态码,返回错误信息等...ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));}
}

1.2 AuthenticationSuccessHandler

这里是我们输入的用户名和密码登录成功后,调用的方法

简单的说就是获取用户信息,使用JWT生成token,然后返回token

/*** 登录成功处理类,登录成功后会调用里面的方法* @author Exrickx*/
@Slf4j
@Component
public class CustomerAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {//简单的说就是获取当前用户,拿到用户名或者userId,创建token,返回log.info("登陆成功...");CustomerUserDetails principal = (CustomerUserDetails) authentication.getPrincipal();//颁发tokenMap<String,Object> emptyMap = new HashMap<>(4);emptyMap.put(UserConstants.USER_ID,principal.getId());String token = JwtTokenUtil.generateToken(principal.getUsername(), emptyMap);ResponseUtil.out(ResultUtil.success(token));}
}

1.3 AuthenticationFailureHandler

有登陆成功就有登录失败

登录失败的时候调用这个方法,可以在其中做登录错误限制或者其他操作,我这里直接就是设置响应头的状态码为401,返回

/*** 登录账号密码错误等情况下,会调用的处理类* @author Exrickx*/
@Slf4j
@Component
public class CustomerAuthenticationFailHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {//设置response状态码,返回错误信息等....ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.LOGIN_UNMATCH_ERROR));}}

1.4 LogoutSuccessHandler

登出注销的时候调用,Jwt有个缺点就是无法主动控制失效,可以采用Jwt+session的方式,比如删除存储在Redis的token

这里需要注意,如果将SpringSecurity的session配置为无状态,或者不保存session,这里authentication为null!!,注意空指针问题。(详情见下面的配置WebSecurityConfigurerAdapter)

/*** 登出成功的调用类* @author zzzgd*/
@Component
public class CustomerLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {ResponseUtil.out(ResultUtil.success("Logout Success!"));}
}

1.5 AccessDeniedHandler

登录后,访问缺失权限的资源会调用。

/*** 没有权限,被拒绝访问时的调用类* @author Exrickx*/
@Component
@Slf4j
public class CustomerRestAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {ResponseUtil.out(403, ResultUtil.failure(ErrorCodeConstants.PERMISSION_DENY));}}

1.6 一个过滤器OncePerRequestFilter

这里算是一个小重点。

上面我们在登录成功后,返回了一个token,那怎么使用这个token呢?

前端发起请求的时候将token放在请求头中,在过滤器中对请求头进行解析。

  • 如果有accessToken的请求头(可以自已定义名字),取出token,解析token,解析成功说明token正确,将解析出来的用户信息放到SpringSecurity的上下文中

  • 如果有accessToken的请求头,解析token失败(无效token,或者过期失效),取不到用户信息,放行

  • 没有accessToken的请求头,放行

这里可能有人会疑惑,为什么token失效都要放行呢?

这是因为SpringSecurity会自己去做登录的认证和权限的校验,靠的就是我们放在SpringSecurity上下文中的SecurityContextHolder.getContext().setAuthentication(authentication);,没有拿到authentication,放行了,SpringSecurity还是会走到认证和校验,这个时候就会发现没有登录没有权限。

旧版本, 最新在底部

package com.zgd.shop.web.config.auth.filter;import com.zgd.shop.common.constants.SecurityConstants;
import com.zgd.shop.common.util.jwt.JwtTokenUtil;
import com.zgd.shop.web.config.auth.user.CustomerUserDetailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 过滤器,在请求过来的时候,解析请求头中的token,再解析token得到用户信息,再存到SecurityContextHolder中* @author zzzgd*/
@Component
@Slf4j
public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {@AutowiredCustomerUserDetailService customerUserDetailService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {//请求头为 accessToken//请求体为 Bearer tokenString authHeader = request.getHeader(SecurityConstants.HEADER);if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {final String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());String username = JwtTokenUtil.parseTokenGetUsername(authToken);if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);if (userDetails != null) {UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);}}}chain.doFilter(request, response);}
}

1.7 实现UserDetails扩充字段

这个接口表示的用户信息,SpringSecurity默认实现了一个User,不过字段寥寥无几,只有username,password这些,而且后面获取用户信息的时候也是获取的UserDetail。

于是我们将自己的数据库的User作为拓展,自己实现这个接口。继承的是数据库对应的User,而不是SpringSecurity的User

package com.zgd.shop.web.config.auth.user;import com.zgd.shop.common.constants.UserConstants;
import com.zgd.shop.dao.entity.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;/*** CustomerUserDetails** @author zgd* @date 2019/7/17 15:29*/
public class CustomerUserDetails extends User implements UserDetails {private Collection<? extends GrantedAuthority> authorities;public CustomerUserDetails(User user){this.setId(user.getId());this.setUsername(user.getUsername());this.setPassword(user.getPassword());this.setRoles(user.getRoles());this.setStatus(user.getStatus());}public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {this.authorities = authorities;}/*** 添加用户拥有的权限和角色* @return*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}/*** 账户是否过期* @return*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 是否禁用* @return*/@Overridepublic boolean isAccountNonLocked() {return  true;}/*** 密码是否过期* @return*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 是否启用* @return*/@Overridepublic boolean isEnabled() {return UserConstants.USER_STATUS_NORMAL.equals(this.getStatus());}
}

1.8 实现UserDetailsService

SpringSecurity在登录的时候,回去数据库(或其他来源),根据username获取正确的user信息,就会根据这个service类,拿到用户的信息和权限。我们自己实现

package com.zgd.shop.web.config.auth.user;import com.alibaba.fastjson.JSON;
import com.zgd.shop.dao.entity.model.User;
import com.zgd.shop.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;/*** @author zgd* @date 2019/1/16 16:27* @description 自己实现UserDetailService,用与SpringSecurity获取用户信息*/
@Service
@Slf4j
public class CustomerUserDetailService implements UserDetailsService {@Autowiredprivate IUserService userService;/*** 获取用户信息,然后交给spring去校验权限* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//获取用户信息User user = userService.getUserRoleByUserName(username);if(user == null){throw new UsernameNotFoundException("用户名不存在");}CustomerUserDetails customerUserDetails = new CustomerUserDetails(user);List<SimpleGrantedAuthority> authorities = new ArrayList<>();//用于添加用户的权限。只要把用户权限添加到authorities 就万事大吉。if (CollectionUtils.isNotEmpty(user.getRoles())){user.getRoles().forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_"+r.getRoleName())));}customerUserDetails.setAuthorities(authorities);log.info("authorities:{}", JSON.toJSONString(authorities));//这里返回的是我们自己定义的UserDetailreturn customerUserDetails;}
}

二、配置WebSecurityConfigurerAdapter

我们需要将上面定义的handler和filter,注册到SpringSecurity。同时配置一些放行的url

这里有一点需要注意:如果配置了下面的SessionCreationPolicy.STATELESS,则SpringSecurity不会保存session会话,在/logout登出的时候会拿不到用户实体对象。

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

如果登出注销不依赖SpringSecurity,并且session交给redis的token来管理的话,可以按上面的配置。

package com.zgd.shop.web.config;import com.zgd.shop.web.config.auth.encoder.MyAesPasswordEncoder;
import com.zgd.shop.web.config.auth.encoder.MyEmptyPasswordEncoder;
import com.zgd.shop.web.config.auth.handler.*;
import com.zgd.shop.web.config.auth.filter.CustomerJwtAuthenticationTokenFilter;
import com.zgd.shop.web.config.auth.user.CustomerUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;/*** @Author: zgd* @Date: 2019/1/15 17:42* @Description:*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)// 控制@Secured权限注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {/*** 这里需要交给spring注入,而不是直接new*/@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate CustomerUserDetailService customerUserDetailService;@Autowiredprivate CustomerAuthenticationFailHandler customerAuthenticationFailHandler;@Autowiredprivate CustomerAuthenticationSuccessHandler customerAuthenticationSuccessHandler;@Autowiredprivate CustomerJwtAuthenticationTokenFilter customerJwtAuthenticationTokenFilter;@Autowiredprivate CustomerRestAccessDeniedHandler customerRestAccessDeniedHandler;@Autowiredprivate CustomerLogoutSuccessHandler customerLogoutSuccessHandler;@Autowiredprivate CustomerAuthenticationEntryPoint customerAuthenticationEntryPoint;/*** 该方法定义认证用户信息获取的来源、密码校验的规则** @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//auth.authenticationProvider(myauthenticationProvider)  自定义密码校验的规则//如果需要改变认证的用户信息来源,我们可以实现UserDetailsServiceauth.userDetailsService(customerUserDetailService).passwordEncoder(passwordEncoder);}@Overrideprotected void configure(HttpSecurity http) throws Exception {/*** antMatchers: ant的通配符规则* ? 匹配任何单字符* * 匹配0或者任意数量的字符,不包含"/"* ** 匹配0或者更多的目录,包含"/"*/http.headers().frameOptions().disable();http//登录后,访问没有权限处理类.exceptionHandling().accessDeniedHandler(customerRestAccessDeniedHandler)//匿名访问,没有权限的处理类.authenticationEntryPoint(customerAuthenticationEntryPoint);//使用jwt的Authentication,来解析过来的请求是否有tokenhttp.addFilterBefore(customerJwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);http.authorizeRequests()//这里表示"/any"和"/ignore"不需要权限校验.antMatchers("/ignore/**", "/login", "/**/register/**").permitAll().anyRequest().authenticated()// 这里表示任何请求都需要校验认证(上面配置的放行).and()//配置登录,检测到用户未登录时跳转的url地址,登录放行.formLogin()//需要跟前端表单的action地址一致.loginProcessingUrl("/login").successHandler(customerAuthenticationSuccessHandler).failureHandler(customerAuthenticationFailHandler).permitAll()//配置取消session管理,又Jwt来获取用户状态,否则即使token无效,也会有session信息,依旧判断用户为登录状态.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//配置登出,登出放行.and().logout().logoutSuccessHandler(customerLogoutSuccessHandler).permitAll().and().csrf().disable();}}

三、其他

大概到这就差不多了,启动,localhost:8080/login,使用postman,采用form-data,post提交,参数是username和password,调用,返回token。

将token放在header中,请求接口。

3.1 不足之处

上面是最简单的处理,还有很多优化的地方。比如

  • 控制token销毁?

使用redis+token组合,不仅解析token,还判断redis是否有这个token。注销和主动失效token:删除redis的key

  • 控制token过期时间?如果用户在token过期前1秒还在操作,下1秒就需要重新登录,肯定不好

1、考虑加入refreshToken,过期时间比token长,前端在拿到token的同时获取过期时间,在过期前一分钟用refreshToken调用refresh接口,重新获取新的token。

2、 将返回的jwtToken设置短一点的过期时间,redis再存这个token,过期时间设置长一点。如果请求过来token过期,查询redis,如果redis还存在,返回新的token。(为什么redis的过期时间大于token的?因为redis的过期是可控的,手动可删除,以redis的为准)

  • 每次请求都会被OncePerRequestFilter 拦截,每次都会被UserDetailService中的获取用户数据请求数据库

可以考虑做缓存,还是用redis或者直接保存内存中

3.2 解决

这是针对上面的2.2说的,也就是redis时间久一点,jwt过期后如果redis没过期,颁发新的jwt。

不过更推荐的是前端判断过期时间,在过期之前调用refresh接口拿到新的jwt。

为什么这样?

如果redis过期时间是一周,jwt是一个小时,那么一个小时后,拿着这个过期的jwt去调,就可以想创建多少个新的jwt就创建,只要没过redis的过期时间。当然这是在没对过期的jwt做限制的情况下,如果要考虑做限制,比如对redis的value加一个字段,保存当前jwt,刷新后就用新的jwt覆盖,refresh接口判断当前的过期jwt是不是和redis这个一样。

总之还需要判断刷新token的时候,过期jwt是否合法的问题。总不能去年的过期token也拿来刷新吧。

而在过期前去刷新token的话,至少不会发生这种事情

不过我这里自己写demo,采用的还是2.2的方式,也就是过期后给个新的,思路如下:

  • 登录后颁发token,token有个时间戳,同时以username拼装作为key,保存这个时间戳到缓存(redis,cache)

  • 请求来了,过滤器解析token,没过期的话,还需要比较缓存中的时间戳和token的时间戳是不是一样 ,如果时间戳不一样,说明该token不能刷新。无视

  • 注销,清除缓存数据

这样就可以避免token过期后,我还能拿到这个token无限制的refresh。

不过这个还是有细节方面问题,并发下同时刷新token这些并没有考虑,部分代码如下

旧版本, 最新在底部

package com.zgd.shop.web.auth.filter;import com.zgd.shop.common.constants.SecurityConstants;
import com.zgd.shop.common.util.jwt.JwtTokenUtil;
import com.zgd.shop.web.auth.user.CustomerUserDetailService;
import com.zgd.shop.web.auth.user.CustomerUserDetails;
import com.zgd.shop.web.auth.user.UserSessionService;
import com.zgd.shop.web.auth.user.UserTokenManager;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 过滤器,在请求过来的时候,解析请求头中的token,再解析token得到用户信息,再存到SecurityContextHolder中* @author zzzgd*/
@Component
@Slf4j
public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {@AutowiredCustomerUserDetailService customerUserDetailService;@AutowiredUserSessionService userSessionService;@AutowiredUserTokenManager userTokenManager;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {//请求头为 accessToken//请求体为 Bearer tokenString authHeader = request.getHeader(SecurityConstants.HEADER);if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {final String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());String username;Claims claims;try {claims = JwtTokenUtil.parseToken(authToken);username = claims.getSubject();} catch (ExpiredJwtException e) {//token过期claims = e.getClaims();username = claims.getSubject();CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);if (userDetails != null){//session未过期,比对时间戳是否一致,是则重新颁发tokenif (isSameTimestampToken(username,e.getClaims())){userTokenManager.awardAccessToken(userDetails,true);}}}//避免每次请求都请求数据库查询用户信息,从缓存中查询CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//                UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);if (userDetails != null) {if(isSameTimestampToken(username,claims)){//必须token解析的时间戳和session保存的一致UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);}}}}chain.doFilter(request, response);}/*** 判断是否同一个时间戳* @param username * @param claims* @return*/private boolean isSameTimestampToken(String username, Claims claims){Long timestamp = userSessionService.getTokenTimestamp(username);Long jwtTimestamp = (Long) claims.get(SecurityConstants.TIME_STAMP);return timestamp.equals(jwtTimestamp);}
}
package com.zgd.shop.web.auth.user;import com.google.common.collect.Maps;
import com.zgd.shop.common.constants.SecurityConstants;
import com.zgd.shop.common.constants.UserConstants;
import com.zgd.shop.common.util.ResponseUtil;
import com.zgd.shop.common.util.jwt.JwtTokenUtil;
import com.zgd.shop.core.result.ResultUtil;
import com.zgd.shop.web.config.auth.UserAuthProperties;
import org.apache.commons.collections.MapUtils;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;/*** UserTokenManager* token管理** @author zgd* @date 2019/7/19 15:25*/
@Component
public class UserTokenManager {@Autowiredprivate UserAuthProperties userAuthProperties;@Autowiredprivate UserSessionService userSessionService;/*** 颁发token* @param principal* @author zgd* @date 2019/7/19 15:34* @return void*/public void awardAccessToken(CustomerUserDetails principal,boolean isRefresh) {//颁发token 确定时间戳,保存在session中和token中long mill = System.currentTimeMillis();userSessionService.saveSession(principal);userSessionService.saveTokenTimestamp(principal.getUsername(),mill);Map<String,Object> param = new HashMap<>(4);param.put(UserConstants.USER_ID,principal.getId());param.put(SecurityConstants.TIME_STAMP,mill);String token = JwtTokenUtil.generateToken(principal.getUsername(), param,userAuthProperties.getJwtExpirationTime());HashMap<String, String> map = Maps.newHashMapWithExpectedSize(1);map.put(SecurityConstants.HEADER,token);int code = isRefresh ? 201 : 200;ResponseUtil.outWithHeader(code,ResultUtil.success(),map);}
}

针对token解析的过滤器做了优化:

  • 如果redis的session没过期, 但是请求头的token过期了, 判断时间戳一致后, 颁发新token并返回

  • 如果redis的session没过期, 但是请求头的token过期了, 时间戳不一致, 说明当前请求的token无法刷新token, 设置响应码为401返回

  • 如果请求头的token过期了, 但是redis的session失效或未找到, 直接放行, 交给后面的权限校验处理(也就是没有给上下文SecurityContextHolder设置登录信息, 后面如果判断这个请求缺少权限会自行处理)

package com.zgd.shop.web.auth.filter;import com.zgd.shop.common.constants.SecurityConstants;
import com.zgd.shop.common.util.ResponseUtil;
import com.zgd.shop.common.util.jwt.JwtTokenUtil;
import com.zgd.shop.core.error.ErrorCodeConstants;
import com.zgd.shop.core.result.ResultUtil;
import com.zgd.shop.web.auth.user.CustomerUserDetailService;
import com.zgd.shop.web.auth.user.CustomerUserDetails;
import com.zgd.shop.web.auth.user.UserSessionService;
import com.zgd.shop.web.auth.user.UserTokenManager;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 过滤器,在请求过来的时候,解析请求头中的token,再解析token得到用户信息,再存到SecurityContextHolder中* @author zzzgd*/
@Component
@Slf4j
public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {@AutowiredCustomerUserDetailService customerUserDetailService;@AutowiredUserSessionService userSessionService;@AutowiredUserTokenManager userTokenManager;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {//请求头为 accessToken//请求体为 Bearer tokenString authHeader = request.getHeader(SecurityConstants.HEADER);if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {//请求头有tokenfinal String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());String username;Claims claims;try {claims = JwtTokenUtil.parseToken(authToken);username = claims.getSubject();} catch (ExpiredJwtException e) {//token过期claims = e.getClaims();username = claims.getSubject();CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);if (userDetails != null){//session未过期,比对时间戳是否一致,是则重新颁发tokenif (isSameTimestampToken(username,e.getClaims())){userTokenManager.awardAccessToken(userDetails,true);//直接设置响应码为201,直接返回return;}else{//时间戳不一致.无效token,无法刷新token,响应码401,前端跳转登录页ResponseUtil.out(HttpStatus.UNAUTHORIZED.value(),ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));return;}}else{//直接放行,交给后面的handler处理,如果当前请求是需要访问权限,则会由CustomerRestAccessDeniedHandler处理chain.doFilter(request, response);return;}}//避免每次请求都请求数据库查询用户信息,从缓存中查询CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//                UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);if (userDetails != null) {if(isSameTimestampToken(username,claims)){//必须token解析的时间戳和session保存的一致UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);}}}}chain.doFilter(request, response);}/*** 判断是否同一个时间戳* @param username* @param claims* @return*/private boolean isSameTimestampToken(String username, Claims claims){Long timestamp = userSessionService.getTokenTimestamp(username);Long jwtTimestamp = (Long) claims.get(SecurityConstants.TIME_STAMP);return timestamp.equals(jwtTimestamp);}
}
热门内容:
  • 别再用 BeanUtils 了,这款 PO VO DTO 转换神器不香么?

  • 抗住双11超高并发,今年排第一的调优方案,没有之一!

  • 妙用Java 8中的 Function接口 消灭if...else(非常新颖的写法)

  • 阿里二面:现针对一个请求量10000次/s的秒杀系统,说说你的优化思路

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)ノ♡

SpringBoot+SpringSecurity前后端分离+Jwt的权限认证(改造记录)相关推荐

  1. SpringBoot+SpringSecurity前后端分离+Jwt的权限认证改造记录!

    大家好,我是宝哥! 前言 一般来说,我们用SpringSecurity默认的话是前后端整在一起的,比如thymeleaf或者Freemarker,SpringSecurity还自带login登录页,还 ...

  2. SpringBoot+SpringSecurity前后端分离+Jwt的权限认证

    前言 一般来说,我们用SpringSecurity默认的话是前后端整在一起的,比如thymeleaf或者Freemarker,SpringSecurity还自带login登录页,还让你配置登出页,错误 ...

  3. SpringSecurity前后端分离下对登录认证的管理

    本案例基于springboot2.0.4,只说对登录验证的管理,不涉及权限的管理.因为学习新东西一下子太多,很容易乱.        首先添加maven依赖,直接开启springboot自带的spri ...

  4. SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题(六)

    当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异.笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,我想通过5-6篇文章,来介绍一下项目中遇到的问题以及我的解决方案 ...

  5. SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题(二)

    关注公众号[江南一点雨],专注于 Spring Boot+微服务以及前后端分离等全栈技术,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货! 当前后端分离时,权限问题的 ...

  6. Springboot+vue前后端分离考试系统

    作者主页:编程指南针 简介:Java领域优质创作者.CSDN博客专家  Java项目.简历模板.学习资料.面试题库.技术互助 文末获取源码 项目编号:BS-XX-104 指南针考试系统是一个多角色在线 ...

  7. SpringBoot+vue前后端分离博客项目

    SpringBoot+vue前后端分离博客项目 Java后端接口开发 1.前言 2.新建Springboot项目 3.整合mybatis plus 第一步:导入jar包 第二步:然后去写配置文件: 第 ...

  8. SpringSecurity - 前后端分离简单实战 - 环境准备

    SpringSecurity 学习指南大全 文章目录 SpringSecurity - 前后端分离简单实战 - 环境准备 最好的理解方法 环境准备 技术前提 项目架构 项目创建 项目配置 数据库配置 ...

  9. SpringSecurity前后端分离(包含token和验证码登录)

    SpringSecurity前后端分离 从上至下操作,直接上手SpringSecurity 文章目录 SpringSecurity前后端分离 1.项目环境 maven依赖 数据库表 2.自定义User ...

最新文章

  1. Codeforces 337D Book of Evil:树的直径【结论】
  2. Http协议中Get和Post的浅谈
  3. oracle服务端用sql查看连接到数据库的IP
  4. myeclipse 项目右键没有svn_新建SVN仓库并上传项目
  5. 网络设备监控-Catic添加H3C的监控图解
  6. yarn add webpack webpack-cli 报错
  7. 计算机的网络操作题,计算机网络操作题
  8. c语言中如何使用面向对象编程,如何使用C语言的面向对象
  9. android volume挂载流程,Android SDCard UnMounted 流程分析(一)
  10. wpf的listview编辑指定列_将项目添加到WPF ListView中的列
  11. Maven项目添加ojdbc8
  12. 拓端tecdat|数据类岗位需求的数据面
  13. 没有U盘纯硬盘安装linux之Ubuntu22.04
  14. 计算机数学位数,有效位数
  15. AWFWD改进后的融合算法
  16. C#:List泛型集合
  17. 基层管理者必备的能力和素质
  18. jzoj 高中 1285——奶酪厂
  19. Cesium开发高级篇 | 05场景后期处理
  20. 频繁跳槽,这谁顶得住~

热门文章

  1. 自动驾驶安全框架开发进展综述
  2. 汽车中的ECU, VCU, MCU, HCU
  3. ORACLE中的imp和exp
  4. SQL中内连接、外连接、交叉连接
  5. 如何利用pyecharts绘制炫酷的关系网络图?
  6. 特斯拉Model 3国内起售价下调至23.59万元
  7. Kaggle金牌得主的Python数据挖掘框架,机器学习基本流程都讲清楚了
  8. 针对《评人工智能如何走向新阶段》一文,继续发布国内外的跟贴留言439-448条如下:
  9. 《评人工智能如何走向新阶段》后记(再续12)
  10. AI研究过于集中狭隘,我们是不是该反思了?