refactor: 优化安全配置和代码结构,移除冗余类,SecurityUtils优化获取用户名与用户ID方式

pull/872/head
Jie Zheng 2025-01-15 16:16:30 +08:00
parent 75df46b5dc
commit 51d9f42273
24 changed files with 281 additions and 280 deletions

View File

@ -22,7 +22,7 @@ import lombok.Data;
* @date 2018-11-23
*/
@Data
class ApiError {
public class ApiError {
private Integer status = 400;
private Long timestamp;

View File

@ -15,18 +15,24 @@
*/
package me.zhengjie.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.exception.BadRequestException;
import me.zhengjie.utils.enums.DataScopeEnum;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Objects;
/**
*
@ -34,8 +40,23 @@ import java.util.List;
* @date 2019-01-17
*/
@Slf4j
@Component
public class SecurityUtils {
public static String header;
public static String tokenStartWith;
@Value("${jwt.header}")
public void setHeader(String header) {
SecurityUtils.header = header;
}
@Value("${jwt.token-start-with}")
public void setTokenStartWith(String tokenStartWith) {
SecurityUtils.tokenStartWith = tokenStartWith;
}
/**
*
* @return UserDetails
@ -45,34 +66,6 @@ public class SecurityUtils {
return userDetailsService.loadUserByUsername(getCurrentUsername());
}
/**
*
*
* @return
*/
public static String getCurrentUsername() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new BadRequestException(HttpStatus.UNAUTHORIZED, "当前登录状态过期");
}
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
throw new BadRequestException(HttpStatus.UNAUTHORIZED, "找不到当前登录的信息");
}
/**
* ID
* @return ID
*/
public static Long getCurrentUserId() {
UserDetails userDetails = getCurrentUser();
// 将 Java 对象转换为 JSONObject 对象
JSONObject jsonObject = (JSONObject) JSON.toJSON(userDetails);
return jsonObject.getJSONObject("user").getLong("id");
}
/**
*
* @return /
@ -91,9 +84,62 @@ public class SecurityUtils {
*/
public static String getDataScopeType() {
List<Long> dataScopes = getCurrentUserDataScope();
if(dataScopes.size() != 0){
if(CollUtil.isEmpty(dataScopes)){
return "";
}
return DataScopeEnum.ALL.getValue();
}
/**
* ID
* @return ID
*/
public static Long getCurrentUserId() {
return getCurrentUserId(getToken());
}
/**
* ID
* @return ID
*/
public static Long getCurrentUserId(String token) {
JWT jwt = JWTUtil.parseToken(token);
return Long.valueOf(jwt.getPayload("userId").toString());
}
/**
*
*
* @return
*/
public static String getCurrentUsername() {
return getCurrentUsername(getToken());
}
/**
*
*
* @return
*/
public static String getCurrentUsername(String token) {
JWT jwt = JWTUtil.parseToken(token);
return jwt.getPayload("sub").toString();
}
/**
* Token
* @return /
*/
public static String getToken() {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder
.getRequestAttributes())).getRequest();
String bearerToken = request.getHeader(header);
if (bearerToken != null && bearerToken.startsWith(tokenStartWith)) {
// 去掉令牌前缀
return bearerToken.replace(tokenStartWith, "");
} else {
log.debug("非法Token{}", bearerToken);
}
return null;
}
}

View File

@ -13,89 +13,100 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.modules.security.config.bean;
package me.zhengjie.modules.security.config;
import com.wf.captcha.*;
import com.wf.captcha.base.Captcha;
import lombok.Data;
import me.zhengjie.exception.BadConfigurationException;
import lombok.Getter;
import me.zhengjie.exception.BadRequestException;
import me.zhengjie.modules.security.config.enums.LoginCodeEnum;
import me.zhengjie.utils.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.awt.*;
import java.util.Objects;
/**
*
*
*
* @author liaojinlong
* @date loginCode.length0loginCode.length0/6/10 17:loginCode.length6
* @date 2025-01-13
*/
@Data
public class LoginProperties {
@Configuration
@ConfigurationProperties(prefix = "login.code")
public class CaptchaConfig {
/**
*
*
*/
private boolean singleLogin = false;
private LoginCode loginCode;
public static final String cacheKey = "user-login-cache:";
public boolean isSingleLogin() {
return singleLogin;
}
@Getter
private LoginCodeEnum codeType;
/**
*
*
* @return /
*
*/
public Captcha getCaptcha() {
if (Objects.isNull(loginCode)) {
loginCode = new LoginCode();
if (Objects.isNull(loginCode.getCodeType())) {
loginCode.setCodeType(LoginCodeEnum.ARITHMETIC);
}
}
return switchCaptcha(loginCode);
}
private Long expiration = 5L;
/**
*
*/
private int length = 4;
/**
*
*/
private int width = 111;
/**
*
*/
private int height = 36;
/**
*
*/
private String fontName;
/**
*
*/
private int fontSize = 25;
/**
*
*
* @param loginCode
* @return /
*/
private Captcha switchCaptcha(LoginCode loginCode) {
public Captcha getCaptcha() {
Captcha captcha;
switch (loginCode.getCodeType()) {
switch (codeType) {
case ARITHMETIC:
// 算术类型 https://gitee.com/whvse/EasyCaptcha
captcha = new FixedArithmeticCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha = new FixedArithmeticCaptcha(width, height);
// 几位数运算,默认是两位
captcha.setLen(loginCode.getLength());
captcha.setLen(length);
break;
case CHINESE:
captcha = new ChineseCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
captcha = new ChineseCaptcha(width, height);
captcha.setLen(length);
break;
case CHINESE_GIF:
captcha = new ChineseGifCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
captcha = new ChineseGifCaptcha(width, height);
captcha.setLen(length);
break;
case GIF:
captcha = new GifCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
captcha = new GifCaptcha(width, height);
captcha.setLen(length);
break;
case SPEC:
captcha = new SpecCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
captcha = new SpecCaptcha(width, height);
captcha.setLen(length);
break;
default:
throw new BadConfigurationException("验证码配置信息错误!正确配置查看 LoginCodeEnum ");
throw new BadRequestException("验证码配置信息错误!正确配置查看 LoginCodeEnum ");
}
if(StringUtils.isNotBlank(loginCode.getFontName())){
captcha.setFont(new Font(loginCode.getFontName(), Font.PLAIN, loginCode.getFontSize()));
if(StringUtils.isNotBlank(fontName)){
captcha.setFont(new Font(fontName, Font.PLAIN, fontSize));
}
return captcha;
}

View File

@ -1,43 +0,0 @@
/*
* Copyright 2019-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.modules.security.config;
import me.zhengjie.modules.security.config.bean.LoginProperties;
import me.zhengjie.modules.security.config.bean.SecurityProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @apiNote Pojo
* @author: liaojinlong
* @date: 2020/6/10 19:04
*/
@Configuration
public class ConfigBeanConfiguration {
@Bean
@ConfigurationProperties(prefix = "login")
public LoginProperties loginProperties() {
return new LoginProperties();
}
@Bean
@ConfigurationProperties(prefix = "jwt")
public SecurityProperties securityProperties() {
return new SecurityProperties();
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2019-2020 the original author or authors.
*
* Licensed under the Apache License, Version loginCode.length.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-loginCode.length.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.modules.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
*
*
* @author liaojinlong
* @date loginCode.length0loginCode.length0/6/10 17:loginCode.length6
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "login")
public class LoginProperties {
/**
*
*/
private boolean singleLogin = false;
public static final String cacheKey = "user-login-cache:";
}

View File

@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.modules.security.config.bean;
package me.zhengjie.modules.security.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* Jwt
@ -24,6 +26,8 @@ import lombok.Data;
* @date 20191128
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class SecurityProperties {
/**

View File

@ -16,7 +16,6 @@
package me.zhengjie.modules.security.config;
import lombok.RequiredArgsConstructor;
import me.zhengjie.modules.security.config.bean.SecurityProperties;
import me.zhengjie.modules.security.security.*;
import me.zhengjie.modules.security.service.OnlineUserService;
import me.zhengjie.modules.security.service.UserCacheManager;
@ -34,7 +33,6 @@ import org.springframework.security.config.core.GrantedAuthorityDefaults;
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;
import org.springframework.web.filter.CorsFilter;
import java.util.*;
@ -54,7 +52,6 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
private final ApplicationContext applicationContext;
private final SecurityProperties properties;
private final OnlineUserService onlineUserService;
private final UserCacheManager userCacheManager;
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
@ -75,7 +72,7 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
httpSecurity
// 禁用 CSRF
.csrf().disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.addFilter(corsFilter)
// 授权异常
.exceptionHandling()
.authenticationEntryPoint(authenticationErrorHandler)
@ -131,6 +128,6 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
}
private TokenConfigurer securityConfigurerAdapter() {
return new TokenConfigurer(tokenProvider, properties, onlineUserService, userCacheManager);
return new TokenConfigurer(tokenProvider, properties, onlineUserService);
}
}

View File

@ -1,61 +0,0 @@
/*
* Copyright 2019-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.modules.security.config.bean;
import lombok.Data;
/**
*
*
* @author liaojinlong
* @date 2020/6/10 18:53
*/
@Data
public class LoginCode {
/**
*
*/
private LoginCodeEnum codeType;
/**
*
*/
private Long expiration = 2L;
/**
*
*/
private int length = 2;
/**
*
*/
private int width = 111;
/**
*
*/
private int height = 36;
/**
*
*/
private String fontName;
/**
*
*/
private int fontSize = 25;
public LoginCodeEnum getCodeType() {
return codeType;
}
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.modules.security.config.bean;
package me.zhengjie.modules.security.config.enums;
/**
*

View File

@ -27,10 +27,12 @@ import me.zhengjie.annotation.rest.AnonymousGetMapping;
import me.zhengjie.annotation.rest.AnonymousPostMapping;
import me.zhengjie.config.properties.RsaProperties;
import me.zhengjie.exception.BadRequestException;
import me.zhengjie.modules.security.config.bean.LoginCodeEnum;
import me.zhengjie.modules.security.config.bean.LoginProperties;
import me.zhengjie.modules.security.config.bean.SecurityProperties;
import me.zhengjie.modules.security.config.CaptchaConfig;
import me.zhengjie.modules.security.config.enums.LoginCodeEnum;
import me.zhengjie.modules.security.config.LoginProperties;
import me.zhengjie.modules.security.config.SecurityProperties;
import me.zhengjie.modules.security.security.TokenProvider;
import me.zhengjie.modules.security.service.UserDetailsServiceImpl;
import me.zhengjie.modules.security.service.dto.AuthUserDto;
import me.zhengjie.modules.security.service.dto.JwtUserDto;
import me.zhengjie.modules.security.service.OnlineUserService;
@ -41,13 +43,12 @@ import me.zhengjie.utils.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@ -63,14 +64,15 @@ import java.util.concurrent.TimeUnit;
@RequestMapping("/auth")
@RequiredArgsConstructor
@Api(tags = "系统:系统授权接口")
public class AuthorizationController {
public class AuthController {
private final SecurityProperties properties;
private final RedisUtils redisUtils;
private final OnlineUserService onlineUserService;
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
@Resource
private LoginProperties loginProperties;
private final LoginProperties loginProperties;
private final CaptchaConfig captchaConfig;
private final PasswordEncoder passwordEncoder;
private final UserDetailsServiceImpl userDetailsService;
@Log("用户登录")
@ApiOperation("登录授权")
@ -88,27 +90,29 @@ public class AuthorizationController {
if (StringUtils.isBlank(authUser.getCode()) || !authUser.getCode().equalsIgnoreCase(code)) {
throw new BadRequestException("验证码错误");
}
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 获取用户信息
JwtUserDto jwtUser = userDetailsService.loadUserByUsername(authUser.getUsername());
// 验证用户密码
if (!passwordEncoder.matches(password, jwtUser.getPassword())) {
throw new BadRequestException("登录密码错误");
}
Authentication authentication = new UsernamePasswordAuthenticationToken(jwtUser, null, jwtUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌与第三方系统获取令牌方式
// UserDetails userDetails = userDetailsService.loadUserByUsername(userInfo.getUsername());
// Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// SecurityContextHolder.getContext().setAuthentication(authentication);
String token = tokenProvider.createToken(authentication);
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
// 生成令牌
String token = tokenProvider.createToken(jwtUser);
// 将密码设置为空
jwtUser.setPassword(null);
// 返回 token 与 用户信息
Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
put("token", properties.getTokenStartWith() + token);
put("user", jwtUserDto);
put("user", jwtUser);
}};
if (loginProperties.isSingleLogin()) {
// 踢掉之前已经登录的token
onlineUserService.kickOutForUsername(authUser.getUsername());
}
// 保存在线信息
onlineUserService.save(jwtUserDto, token, request);
onlineUserService.save(jwtUser, token, request);
// 返回登录信息
return ResponseEntity.ok(authInfo);
}
@ -116,14 +120,17 @@ public class AuthorizationController {
@ApiOperation("获取用户信息")
@GetMapping(value = "/info")
public ResponseEntity<UserDetails> getUserInfo() {
return ResponseEntity.ok(SecurityUtils.getCurrentUser());
JwtUserDto jwtUser = (JwtUserDto) SecurityUtils.getCurrentUser();
// 将密码设置为空
jwtUser.setPassword(null);
return ResponseEntity.ok(jwtUser);
}
@ApiOperation("获取验证码")
@AnonymousGetMapping(value = "/code")
public ResponseEntity<Object> getCode() {
// 获取运算的结果
Captcha captcha = loginProperties.getCaptcha();
Captcha captcha = captchaConfig.getCaptcha();
String uuid = properties.getCodeKey() + IdUtil.simpleUUID();
//当验证码类型为 arithmetic时且长度 >= 2 时captcha.text()的结果有几率为浮点型
String captchaValue = captcha.text();
@ -131,7 +138,7 @@ public class AuthorizationController {
captchaValue = captchaValue.split("\\.")[0];
}
// 保存
redisUtils.set(uuid, captchaValue, loginProperties.getLoginCode().getExpiration(), TimeUnit.MINUTES);
redisUtils.set(uuid, captchaValue, captchaConfig.getExpiration(), TimeUnit.MINUTES);
// 验证码信息
Map<String, Object> imgResult = new HashMap<String, Object>(2) {{
put("img", captcha.toBase64());

View File

@ -15,6 +15,9 @@
*/
package me.zhengjie.modules.security.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import me.zhengjie.exception.handler.ApiError;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
@ -32,6 +35,10 @@ public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
//当用户在没有授权的情况下访问受保护的REST资源时将调用此方法发送403 Forbidden响应
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json;charset=UTF-8");
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(ApiError.error(HttpStatus.FORBIDDEN.value(), "禁止访问,您没有权限访问此资源"));
response.getWriter().write(jsonResponse);
}
}

View File

@ -15,10 +15,13 @@
*/
package me.zhengjie.modules.security.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.exception.handler.ApiError;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@ -26,14 +29,18 @@ import java.io.IOException;
/**
* @author Zheng Jie
*/
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
// 当用户尝试访问安全的REST资源而不提供任何凭据时将调用此方法发送401 响应
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException==null?"Unauthorized":authException.getMessage());
int code = HttpStatus.UNAUTHORIZED.value();
response.setStatus(code);
response.setContentType("application/json;charset=UTF-8");
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(ApiError.error(HttpStatus.UNAUTHORIZED.value(), "登录状态已过期,请重新登录"));
response.getWriter().write(jsonResponse);
}
}

View File

@ -16,7 +16,7 @@
package me.zhengjie.modules.security.security;
import lombok.RequiredArgsConstructor;
import me.zhengjie.modules.security.config.bean.SecurityProperties;
import me.zhengjie.modules.security.config.SecurityProperties;
import me.zhengjie.modules.security.service.OnlineUserService;
import me.zhengjie.modules.security.service.UserCacheManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
@ -33,11 +33,10 @@ public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFi
private final TokenProvider tokenProvider;
private final SecurityProperties properties;
private final OnlineUserService onlineUserService;
private final UserCacheManager userCacheManager;
@Override
public void configure(HttpSecurity http) {
TokenFilter customFilter = new TokenFilter(tokenProvider, properties, onlineUserService, userCacheManager);
TokenFilter customFilter = new TokenFilter(tokenProvider, properties, onlineUserService);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}

View File

@ -16,9 +16,7 @@
package me.zhengjie.modules.security.security;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.ExpiredJwtException;
import me.zhengjie.modules.security.config.bean.SecurityProperties;
import me.zhengjie.modules.security.service.UserCacheManager;
import me.zhengjie.modules.security.config.SecurityProperties;
import me.zhengjie.modules.security.service.dto.OnlineUserDto;
import me.zhengjie.modules.security.service.OnlineUserService;
import org.slf4j.Logger;
@ -33,7 +31,6 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Objects;
/**
* @author /
@ -45,19 +42,16 @@ public class TokenFilter extends GenericFilterBean {
private final TokenProvider tokenProvider;
private final SecurityProperties properties;
private final OnlineUserService onlineUserService;
private final UserCacheManager userCacheManager;
/**
* @param tokenProvider Token
* @param properties JWT
* @param onlineUserService 线
* @param userCacheManager
*/
public TokenFilter(TokenProvider tokenProvider, SecurityProperties properties, OnlineUserService onlineUserService, UserCacheManager userCacheManager) {
public TokenFilter(TokenProvider tokenProvider, SecurityProperties properties, OnlineUserService onlineUserService) {
this.properties = properties;
this.onlineUserService = onlineUserService;
this.tokenProvider = tokenProvider;
this.userCacheManager = userCacheManager;
}
@Override
@ -67,24 +61,16 @@ public class TokenFilter extends GenericFilterBean {
String token = resolveToken(httpServletRequest);
// 对于 Token 为空的不需要去查 Redis
if(StrUtil.isNotBlank(token)){
OnlineUserDto onlineUserDto = null;
boolean cleanUserCache = false;
try {
// 获取用户Token的Key
String loginKey = tokenProvider.loginKey(token);
onlineUserDto = onlineUserService.getOne(loginKey);
} catch (ExpiredJwtException e) {
log.error(e.getMessage());
cleanUserCache = true;
} finally {
if (cleanUserCache || Objects.isNull(onlineUserDto)) {
userCacheManager.cleanUserCache(String.valueOf(tokenProvider.getClaims(token).get(TokenProvider.AUTHORITIES_KEY)));
}
}
if (onlineUserDto != null && StringUtils.hasText(token)) {
OnlineUserDto onlineUserDto = onlineUserService.getOne(loginKey);
// 判断用户在线信息是否为空
if (onlineUserDto != null) {
// Token 续期判断
tokenProvider.checkRenewal(token);
// 获取认证信息,设置上下文
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
// Token 续期
tokenProvider.checkRenewal(token);
}
}
filterChain.doFilter(servletRequest, servletResponse);

View File

@ -23,7 +23,8 @@ import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.modules.security.config.bean.SecurityProperties;
import me.zhengjie.modules.security.config.SecurityProperties;
import me.zhengjie.modules.security.service.dto.JwtUserDto;
import me.zhengjie.utils.RedisUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@ -42,11 +43,12 @@ import java.util.concurrent.TimeUnit;
@Component
public class TokenProvider implements InitializingBean {
private final SecurityProperties properties;
private final RedisUtils redisUtils;
public static final String AUTHORITIES_KEY = "user";
private JwtParser jwtParser;
private JwtBuilder jwtBuilder;
private final RedisUtils redisUtils;
private final SecurityProperties properties;
public static final String AUTHORITIES_UUID_KEY = "uuid";
public static final String AUTHORITIES_UID_KEY = "userId";
public TokenProvider(SecurityProperties properties, RedisUtils redisUtils) {
this.properties = properties;
@ -68,15 +70,19 @@ public class TokenProvider implements InitializingBean {
* Token
* Token Redis
*
* @param authentication /
* @param user /
* @return /
*/
public String createToken(Authentication authentication) {
public String createToken(JwtUserDto user) {
// 设置参数
Map<String, Object> claims = new HashMap<>(6);
// 设置用户ID
claims.put(AUTHORITIES_UID_KEY, user.getUser().getId());
// 设置UUID确保每次Token不一样
claims.put(AUTHORITIES_UUID_KEY, IdUtil.simpleUUID());
return jwtBuilder
// 加入ID确保生成的 Token 都不一致
.setId(IdUtil.simpleUUID())
.claim(AUTHORITIES_KEY, authentication.getName())
.setSubject(authentication.getName())
.setClaims(claims)
.setSubject(user.getUsername())
.compact();
}

View File

@ -19,12 +19,11 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.modules.security.security.TokenProvider;
import me.zhengjie.utils.PageResult;
import me.zhengjie.modules.security.config.bean.SecurityProperties;
import me.zhengjie.modules.security.config.SecurityProperties;
import me.zhengjie.modules.security.service.dto.JwtUserDto;
import me.zhengjie.modules.security.service.dto.OnlineUserDto;
import me.zhengjie.utils.*;
import org.springframework.data.domain.Pageable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

View File

@ -16,7 +16,7 @@
package me.zhengjie.modules.security.service;
import cn.hutool.core.util.RandomUtil;
import me.zhengjie.modules.security.config.bean.LoginProperties;
import me.zhengjie.modules.security.config.LoginProperties;
import me.zhengjie.modules.security.service.dto.JwtUserDto;
import me.zhengjie.utils.RedisUtils;
import me.zhengjie.utils.StringUtils;

View File

@ -18,14 +18,12 @@ package me.zhengjie.modules.security.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.exception.BadRequestException;
import me.zhengjie.exception.EntityNotFoundException;
import me.zhengjie.modules.security.service.dto.JwtUserDto;
import me.zhengjie.modules.system.service.DataService;
import me.zhengjie.modules.system.service.RoleService;
import me.zhengjie.modules.system.service.UserService;
import me.zhengjie.modules.system.service.dto.UserLoginDto;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
@ -45,24 +43,14 @@ public class UserDetailsServiceImpl implements UserDetailsService {
public JwtUserDto loadUserByUsername(String username) {
JwtUserDto jwtUserDto = userCacheManager.getUserCache(username);
if(jwtUserDto == null){
UserLoginDto user;
try {
user = userService.getLoginData(username);
} catch (EntityNotFoundException e) {
// SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsException
throw new UsernameNotFoundException(username, e);
}
UserLoginDto user = userService.getLoginData(username);
if (user == null) {
throw new UsernameNotFoundException("");
throw new BadRequestException("用户不存在");
} else {
if (!user.getEnabled()) {
throw new BadRequestException("账号未激活!");
}
jwtUserDto = new JwtUserDto(
user,
dataService.getDeptIds(user),
roleService.mapToGrantedAuthorities(user)
);
jwtUserDto = new JwtUserDto(user, dataService.getDeptIds(user), roleService.buildAuthorities(user), user.getPassword());
// 添加缓存数据
userCacheManager.addUserCache(username, jwtUserDto);
}

View File

@ -16,8 +16,10 @@
package me.zhengjie.modules.security.service.dto;
import com.alibaba.fastjson.annotation.JSONField;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import me.zhengjie.modules.system.service.dto.UserLoginDto;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.List;
@ -38,6 +40,10 @@ public class JwtUserDto implements UserDetails {
private final List<AuthorityDto> authorities;
@Setter
@ApiModelProperty(value = "密码")
private String password;
public Set<String> getRoles() {
return authorities.stream().map(AuthorityDto::getAuthority).collect(Collectors.toSet());
}

View File

@ -120,7 +120,7 @@ public interface RoleService {
* @param user
* @return
*/
List<AuthorityDto> mapToGrantedAuthorities(UserDto user);
List<AuthorityDto> buildAuthorities(UserDto user);
/**
*

View File

@ -15,6 +15,8 @@
*/
package me.zhengjie.modules.system.service.dto;
import com.alibaba.fastjson.annotation.JSONField;
/**
* @author Zheng Jie
* @description 使
@ -22,6 +24,7 @@ package me.zhengjie.modules.system.service.dto;
**/
public class UserLoginDto extends UserDto {
@JSONField(serialize = false)
private String password;
private Boolean isAdmin;

View File

@ -164,7 +164,7 @@ public class RoleServiceImpl implements RoleService {
@Override
@Cacheable(key = "'auth:' + #p0.id")
public List<AuthorityDto> mapToGrantedAuthorities(UserDto user) {
public List<AuthorityDto> buildAuthorities(UserDto user) {
Set<String> permissions = new HashSet<>();
// 如果是管理员直接返回
if (user.getIsAdmin()) {

View File

@ -60,7 +60,7 @@ login:
# 存活时间/秒
idle-time: 21600
# 验证码
login-code:
code:
# 验证码类型配置 查看 LoginProperties 类
code-type: arithmetic
# 登录图形验证码有效时间/分钟

View File

@ -64,7 +64,7 @@ login:
# 存活时间/秒
idle-time: 21600
# 验证码
login-code:
code:
# 验证码类型配置 查看 LoginProperties 类
code-type: arithmetic
# 登录图形验证码有效时间/分钟