security整合JWT
前述
源代码已经上传到 Gitee,对应项目为
jwt
本文在security基于表单提交使用 基础上进行修改
代码中使用的
redis
的有关配置请参考 SpringBoot整合Redis
SpringSecurity整合JWT实现无状态登录
引入依赖
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
yml配置
spring:
redis:
host: localhost
port: 6379
database: 0
password:
timeout: 10s
lettuce:
pool:
min-idle: 0
max-idle: 8
max-active: 8
max-wait: -1ms
datasource:
url: jdbc:mysql://127.0.0.1:3306/security-simple?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: admin
type: com.alibaba.druid.pool.DruidDataSource
thymeleaf:
cache: false
mvc:
static-path-pattern: /static/**
#mybatis-plus 配置
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
type-aliases-package: com.example.jwt.project.sys.entity.*
type-aliases-super-type: java.lang.Object
global-config:
db-config:
id-type: auto
configuration:
aggressive-lazy-loading: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: guhjdsagyuhj5678324hfygf
# 令牌有效期(默认30分钟)
expireTime: 30
新增TokenManager管理器
/**
* @version 1.0.0
* @className: TokenManager
* @description: jwt令牌管理:
* JWT实现大体思路:
* 根据UUID生成一个JWT——token,此 token 并不会设置过期时间,只做数据的记录
* (缓存前缀 + UUID) 作为 Redis 缓存对象的 key,同时 UUID 又是保存在 token 中的,所以 token 也就与 Redis关联了起来
* 只有通过 token 获取到 UUID后,才能在 Redis 中获取到登录对象,如果没有 token,也就获取不到对象,则认证失败,否则成功
* @author: LiJunYi
* @create: 2022/7/27 11:16
*/
@Component
@Slf4j
public class TokenManager
{
/**
* 令牌自定义标识
*/
@Value("${token.header}")
private String header;
/**
* 令牌秘钥
*/
@Value("${token.secret}")
private String secret;
/**
* 令牌有效期(默认30分钟)
*/
@Value("${token.expireTime}")
private int expireTime;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
private final RedisCache redisCache;
public TokenManager(RedisCache redisCache) {
this.redisCache = redisCache;
}
/**
* 从 request 中获取token
* 通过解析token获取用户在redis中缓存的key从而获取到登录用户信息
*
* @param request 请求
* @return {@link LoginUser}
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request);
if (StrUtil.isNotEmpty(token))
{
try
{
Claims claims = parseToken(token);
// 获取UUID
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
// 获取 Redis 缓存中的 key
String userKey = getTokenKey(uuid);
return redisCache.getCacheObject(userKey);
}
catch (Exception ignored)
{
}
}
return null;
}
/**
* 设置用户身份信息
*/
public void setLoginUser(LoginUser loginUser)
{
if (ObjectUtil.isNotNull(loginUser) && StrUtil.isNotEmpty(loginUser.getToken()))
{
refreshToken(loginUser);
}
}
/**
* 删除令牌
*
* @param token 令牌
*/
public void removeToken(String token)
{
if (StrUtil.isNotEmpty(token))
{
String userKey = getTokenKey(token);
redisCache.deleteObject(userKey);
}
}
/**
* 创建令牌
*
* @param loginUser 登录用户
* @return {@link String}
*/
public String createToken(LoginUser loginUser)
{
String uuid = IdUtil.fastUUID();
loginUser.setToken(uuid);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>(8);
claims.put(Constants.LOGIN_USER_KEY, uuid);
return createToken(claims);
}
/**
* 验证令牌有效期,自动刷新缓存
*
* @param loginUser 登录用户
*/
public void verifyToken(LoginUser loginUser)
{
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
{
refreshToken(loginUser);
}
}
/**
* 创建token时存入当前登录用户并同时刷新令牌过期时间
*
* @param loginUser 登录用户
*/
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
/**
* 创建令牌
*
* @param claims 数据声明
* @return {@link String}
*/
public String createToken(Map<String, Object> claims)
{
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token)
{
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 获取令牌
*
* @param request 请求
* @return {@link String}
*/
private String getToken(HttpServletRequest request)
{
String token = request.getHeader(header);
if (StrUtil.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
/**
* 获取令牌在缓存中的key
*
* @param uuid 随机UUID
* @return {@link String}
*/
private String getTokenKey(String uuid)
{
return Constants.LOGIN_TOKEN_KEY + uuid;
}
}
新增JWT自定义认证过滤器
/**
* @version 1.0.0
* @className: JwtAuthenticationTokenFilter
* @description: JWT自定义认证过滤器
* @author: LiJunYi
* @create: 2022/7/27 11:54
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
private final TokenManager tokenManager;
public JwtAuthenticationTokenFilter(TokenManager tokenManager) {
this.tokenManager = tokenManager;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
LoginUser loginUser = tokenManager.getLoginUser(request);
if (ObjectUtil.isNotNull(loginUser) && ObjectUtil.isNull(SecurityUtils.getAuthentication()))
{
tokenManager.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request,response);
}
}
修改退出处理器
/**
* @version 1.0.0
* @className: LogoutSuccessHandlerImpl
* @description: 退出处理器
* @author: LiJunYi
* @create: 2022/7/26 8:34
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
private final TokenManager tokenManager;
public LogoutSuccessHandlerImpl(TokenManager tokenManager)
{
this.tokenManager = tokenManager;
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication e) throws IOException {
LoginUser loginUser = tokenManager.getLoginUser(request);
if(ObjectUtil.isNotNull(loginUser))
{
//移除
tokenManager.removeToken(loginUser.getToken());
}
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
}
}
修改SpringSecurity配置
/**
* @version 1.0.0
* @className: SecurityConfig
* @description: SpringSecurity 5.7.x新用法配置
* @author: LiJunYi
* @create: 2022/7/26 8:43
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig
{
/**
* 自定义用户登录处理逻辑
*/
private final MyUserDetailsServiceImpl userDetailsService;
/**
* 注销成功后处理器
*/
private final LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* 无权限处理器
*/
private final AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 并发登录控制处理器
*/
private final CustomExpiredSessionStrategyImpl expiredSessionStrategy;
/**
* jwt身份验证令牌过滤器
*/
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 安全配置
* 构造函数注入
*
* @param tokenManager 令牌管理器
* @param redisCache 缓存
* @param userDetailsService 用户详细信息服务
* @param logoutSuccessHandler 退出处理器
* @param unauthorizedHandler 无权限处理器
* @param expiredSessionStrategy 并发登录控制处理器
* @param jwtAuthenticationTokenFilter JWT登录过滤器
*/
@Autowired
public SecurityConfig(TokenManager tokenManager, RedisCache redisCache, MyUserDetailsServiceImpl userDetailsService, LogoutSuccessHandlerImpl logoutSuccessHandler,
AuthenticationEntryPointImpl unauthorizedHandler,
CustomExpiredSessionStrategyImpl expiredSessionStrategy,
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
this.userDetailsService = userDetailsService;
this.logoutSuccessHandler = logoutSuccessHandler;
this.unauthorizedHandler = unauthorizedHandler;
this.expiredSessionStrategy = expiredSessionStrategy;
}
/**
* 获取AuthenticationManager(认证管理器),登录时认证使用
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
AuthenticationManager authenticationManager = configuration.getAuthenticationManager();
return configuration.getAuthenticationManager();
}
/**
* 配置加密方式
*
* @return {@link PasswordEncoder}
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 侦听器配置,使 Spring Security 更新有关会话生命周期事件的信息
* 并发会话控制:https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
* @return {@link HttpSessionEventPublisher}
*/
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
/**
* 过滤器链
*
* @param http http
* @return {@link SecurityFilterChain}
* @throws Exception 异常
*/
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception
{
http.authorizeRequests(authorize ->
authorize.mvcMatchers("/userLogin","/noPermission").permitAll()
.anyRequest().authenticated()
)
.csrf().disable()
// 基于token,不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers().frameOptions().disable();
// 退出过滤器
http.logout(logout -> logout
.logoutUrl("/logout")
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(logoutSuccessHandler));
// 添加JWT filter
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// by default uses a Bean by the name of corsConfigurationSource
http.cors(withDefaults());
// 无权限处理器
http.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedPage("/noPermission");
// 并发会话控制
http.sessionManagement(session -> session
.maximumSessions(1)
.expiredSessionStrategy(expiredSessionStrategy));
http.userDetailsService(userDetailsService);
return http.build();
}
/**
* 配置跨源访问(CORS)
* 官方文档:https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html
* @return {@link CorsConfigurationSource}
*/
@Bean
CorsConfigurationSource corsConfigurationSource()
{
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080/","https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
修改登录接口
/**
* 登录控制器
*
* @author LiJunYi
* @date 2022/07/26
*/
@RestController
public class LoginController
{
@Autowired
ISysUserService userService;
@Autowired
SysLoginService loginService;
/**
* 登录方法
*
* @param user 用户
* @return {@link AjaxResult}
*/
@PostMapping("userLogin")
public AjaxResult login(SysUser user)
{
if (StrUtil.isEmpty(user.getUsername()) || StrUtil.isEmpty(user.getPassword()))
{
return AjaxResult.error("用户名或密码未输入!");
}
String token = loginService.login(user.getUsername(),user.getPassword());
return AjaxResult.success(Constants.TOKEN, token);
}
}
登录业务逻辑代码
/**
* @version 1.0.0
* @className: SysLoginService
* @description: 登录业务
* @author: LiJunYi
* @create: 2022/7/27 12:53
*/
@Component
public class SysLoginService
{
@Autowired
private TokenManager tokenManager;
@Resource
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Autowired
private ISysUserService userService;
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @return 结果
*/
public String login(String username, String password)
{
// 用户验证
Authentication authentication;
try
{
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
throw new ServiceException("用户名密码错误");
}
else
{
throw new ServiceException(e.getMessage());
}
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenManager.createToken(loginUser);
}
}