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 * @date 2018-11-23
*/ */
@Data @Data
class ApiError { public class ApiError {
private Integer status = 400; private Integer status = 400;
private Long timestamp; private Long timestamp;

View File

@ -15,18 +15,24 @@
*/ */
package me.zhengjie.utils; 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.JSON;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.zhengjie.exception.BadRequestException;
import me.zhengjie.utils.enums.DataScopeEnum; import me.zhengjie.utils.enums.DataScopeEnum;
import org.springframework.http.HttpStatus; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; 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.List;
import java.util.Objects;
/** /**
* *
@ -34,8 +40,23 @@ import java.util.List;
* @date 2019-01-17 * @date 2019-01-17
*/ */
@Slf4j @Slf4j
@Component
public class SecurityUtils { 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 * @return UserDetails
@ -45,34 +66,6 @@ public class SecurityUtils {
return userDetailsService.loadUserByUsername(getCurrentUsername()); 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 / * @return /
@ -91,9 +84,62 @@ public class SecurityUtils {
*/ */
public static String getDataScopeType() { public static String getDataScopeType() {
List<Long> dataScopes = getCurrentUserDataScope(); List<Long> dataScopes = getCurrentUserDataScope();
if(dataScopes.size() != 0){ if(CollUtil.isEmpty(dataScopes)){
return ""; return "";
} }
return DataScopeEnum.ALL.getValue(); 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 * See the License for the specific language governing permissions and
* limitations under the License. * 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.*;
import com.wf.captcha.base.Captcha; import com.wf.captcha.base.Captcha;
import lombok.Data; 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 me.zhengjie.utils.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.awt.*; import java.awt.*;
import java.util.Objects;
/** /**
* *
*
* @author liaojinlong * @author liaojinlong
* @date loginCode.length0loginCode.length0/6/10 17:loginCode.length6 * @date 2025-01-13
*/ */
@Data @Data
public class LoginProperties { @Configuration
@ConfigurationProperties(prefix = "login.code")
public class CaptchaConfig {
/** /**
* *
*/ */
private boolean singleLogin = false; @Getter
private LoginCodeEnum codeType;
private LoginCode loginCode;
public static final String cacheKey = "user-login-cache:";
public boolean isSingleLogin() {
return singleLogin;
}
/** /**
* *
*
* @return /
*/ */
public Captcha getCaptcha() { private Long expiration = 5L;
if (Objects.isNull(loginCode)) {
loginCode = new LoginCode(); /**
if (Objects.isNull(loginCode.getCodeType())) { *
loginCode.setCodeType(LoginCodeEnum.ARITHMETIC); */
} private int length = 4;
}
return switchCaptcha(loginCode); /**
} *
*/
private int width = 111;
/**
*
*/
private int height = 36;
/**
*
*/
private String fontName;
/**
*
*/
private int fontSize = 25;
/** /**
* *
*
* @param loginCode
* @return / * @return /
*/ */
private Captcha switchCaptcha(LoginCode loginCode) { public Captcha getCaptcha() {
Captcha captcha; Captcha captcha;
switch (loginCode.getCodeType()) { switch (codeType) {
case ARITHMETIC: case ARITHMETIC:
// 算术类型 https://gitee.com/whvse/EasyCaptcha // 算术类型 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; break;
case CHINESE: case CHINESE:
captcha = new ChineseCaptcha(loginCode.getWidth(), loginCode.getHeight()); captcha = new ChineseCaptcha(width, height);
captcha.setLen(loginCode.getLength()); captcha.setLen(length);
break; break;
case CHINESE_GIF: case CHINESE_GIF:
captcha = new ChineseGifCaptcha(loginCode.getWidth(), loginCode.getHeight()); captcha = new ChineseGifCaptcha(width, height);
captcha.setLen(loginCode.getLength()); captcha.setLen(length);
break; break;
case GIF: case GIF:
captcha = new GifCaptcha(loginCode.getWidth(), loginCode.getHeight()); captcha = new GifCaptcha(width, height);
captcha.setLen(loginCode.getLength()); captcha.setLen(length);
break; break;
case SPEC: case SPEC:
captcha = new SpecCaptcha(loginCode.getWidth(), loginCode.getHeight()); captcha = new SpecCaptcha(width, height);
captcha.setLen(loginCode.getLength()); captcha.setLen(length);
break; break;
default: default:
throw new BadConfigurationException("验证码配置信息错误!正确配置查看 LoginCodeEnum "); throw new BadRequestException("验证码配置信息错误!正确配置查看 LoginCodeEnum ");
} }
if(StringUtils.isNotBlank(loginCode.getFontName())){ if(StringUtils.isNotBlank(fontName)){
captcha.setFont(new Font(loginCode.getFontName(), Font.PLAIN, loginCode.getFontSize())); captcha.setFont(new Font(fontName, Font.PLAIN, fontSize));
} }
return captcha; 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package me.zhengjie.modules.security.config.bean; package me.zhengjie.modules.security.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/** /**
* Jwt * Jwt
@ -24,6 +26,8 @@ import lombok.Data;
* @date 20191128 * @date 20191128
*/ */
@Data @Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class SecurityProperties { public class SecurityProperties {
/** /**

View File

@ -16,7 +16,6 @@
package me.zhengjie.modules.security.config; package me.zhengjie.modules.security.config;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import me.zhengjie.modules.security.config.bean.SecurityProperties;
import me.zhengjie.modules.security.security.*; import me.zhengjie.modules.security.security.*;
import me.zhengjie.modules.security.service.OnlineUserService; import me.zhengjie.modules.security.service.OnlineUserService;
import me.zhengjie.modules.security.service.UserCacheManager; 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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import java.util.*; import java.util.*;
@ -54,7 +52,6 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final SecurityProperties properties; private final SecurityProperties properties;
private final OnlineUserService onlineUserService; private final OnlineUserService onlineUserService;
private final UserCacheManager userCacheManager;
@Bean @Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() { GrantedAuthorityDefaults grantedAuthorityDefaults() {
@ -75,7 +72,7 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
httpSecurity httpSecurity
// 禁用 CSRF // 禁用 CSRF
.csrf().disable() .csrf().disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) .addFilter(corsFilter)
// 授权异常 // 授权异常
.exceptionHandling() .exceptionHandling()
.authenticationEntryPoint(authenticationErrorHandler) .authenticationEntryPoint(authenticationErrorHandler)
@ -131,6 +128,6 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
} }
private TokenConfigurer securityConfigurerAdapter() { 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 * See the License for the specific language governing permissions and
* limitations under the License. * 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.annotation.rest.AnonymousPostMapping;
import me.zhengjie.config.properties.RsaProperties; import me.zhengjie.config.properties.RsaProperties;
import me.zhengjie.exception.BadRequestException; import me.zhengjie.exception.BadRequestException;
import me.zhengjie.modules.security.config.bean.LoginCodeEnum; import me.zhengjie.modules.security.config.CaptchaConfig;
import me.zhengjie.modules.security.config.bean.LoginProperties; import me.zhengjie.modules.security.config.enums.LoginCodeEnum;
import me.zhengjie.modules.security.config.bean.SecurityProperties; 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.security.TokenProvider;
import me.zhengjie.modules.security.service.UserDetailsServiceImpl;
import me.zhengjie.modules.security.service.dto.AuthUserDto; import me.zhengjie.modules.security.service.dto.AuthUserDto;
import me.zhengjie.modules.security.service.dto.JwtUserDto; import me.zhengjie.modules.security.service.dto.JwtUserDto;
import me.zhengjie.modules.security.service.OnlineUserService; 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.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 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.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -63,14 +64,15 @@ import java.util.concurrent.TimeUnit;
@RequestMapping("/auth") @RequestMapping("/auth")
@RequiredArgsConstructor @RequiredArgsConstructor
@Api(tags = "系统:系统授权接口") @Api(tags = "系统:系统授权接口")
public class AuthorizationController { public class AuthController {
private final SecurityProperties properties; private final SecurityProperties properties;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final OnlineUserService onlineUserService; private final OnlineUserService onlineUserService;
private final TokenProvider tokenProvider; private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder; private final LoginProperties loginProperties;
@Resource private final CaptchaConfig captchaConfig;
private LoginProperties loginProperties; private final PasswordEncoder passwordEncoder;
private final UserDetailsServiceImpl userDetailsService;
@Log("用户登录") @Log("用户登录")
@ApiOperation("登录授权") @ApiOperation("登录授权")
@ -88,27 +90,29 @@ public class AuthorizationController {
if (StringUtils.isBlank(authUser.getCode()) || !authUser.getCode().equalsIgnoreCase(code)) { if (StringUtils.isBlank(authUser.getCode()) || !authUser.getCode().equalsIgnoreCase(code)) {
throw new BadRequestException("验证码错误"); throw new BadRequestException("验证码错误");
} }
UsernamePasswordAuthenticationToken authenticationToken = // 获取用户信息
new UsernamePasswordAuthenticationToken(authUser.getUsername(), password); JwtUserDto jwtUser = userDetailsService.loadUserByUsername(authUser.getUsername());
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); // 验证用户密码
if (!passwordEncoder.matches(password, jwtUser.getPassword())) {
throw new BadRequestException("登录密码错误");
}
Authentication authentication = new UsernamePasswordAuthenticationToken(jwtUser, null, jwtUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌与第三方系统获取令牌方式 // 生成令牌
// UserDetails userDetails = userDetailsService.loadUserByUsername(userInfo.getUsername()); String token = tokenProvider.createToken(jwtUser);
// Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); // 将密码设置为空
// SecurityContextHolder.getContext().setAuthentication(authentication); jwtUser.setPassword(null);
String token = tokenProvider.createToken(authentication);
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
// 返回 token 与 用户信息 // 返回 token 与 用户信息
Map<String, Object> authInfo = new HashMap<String, Object>(2) {{ Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
put("token", properties.getTokenStartWith() + token); put("token", properties.getTokenStartWith() + token);
put("user", jwtUserDto); put("user", jwtUser);
}}; }};
if (loginProperties.isSingleLogin()) { if (loginProperties.isSingleLogin()) {
// 踢掉之前已经登录的token // 踢掉之前已经登录的token
onlineUserService.kickOutForUsername(authUser.getUsername()); onlineUserService.kickOutForUsername(authUser.getUsername());
} }
// 保存在线信息 // 保存在线信息
onlineUserService.save(jwtUserDto, token, request); onlineUserService.save(jwtUser, token, request);
// 返回登录信息 // 返回登录信息
return ResponseEntity.ok(authInfo); return ResponseEntity.ok(authInfo);
} }
@ -116,14 +120,17 @@ public class AuthorizationController {
@ApiOperation("获取用户信息") @ApiOperation("获取用户信息")
@GetMapping(value = "/info") @GetMapping(value = "/info")
public ResponseEntity<UserDetails> getUserInfo() { public ResponseEntity<UserDetails> getUserInfo() {
return ResponseEntity.ok(SecurityUtils.getCurrentUser()); JwtUserDto jwtUser = (JwtUserDto) SecurityUtils.getCurrentUser();
// 将密码设置为空
jwtUser.setPassword(null);
return ResponseEntity.ok(jwtUser);
} }
@ApiOperation("获取验证码") @ApiOperation("获取验证码")
@AnonymousGetMapping(value = "/code") @AnonymousGetMapping(value = "/code")
public ResponseEntity<Object> getCode() { public ResponseEntity<Object> getCode() {
// 获取运算的结果 // 获取运算的结果
Captcha captcha = loginProperties.getCaptcha(); Captcha captcha = captchaConfig.getCaptcha();
String uuid = properties.getCodeKey() + IdUtil.simpleUUID(); String uuid = properties.getCodeKey() + IdUtil.simpleUUID();
//当验证码类型为 arithmetic时且长度 >= 2 时captcha.text()的结果有几率为浮点型 //当验证码类型为 arithmetic时且长度 >= 2 时captcha.text()的结果有几率为浮点型
String captchaValue = captcha.text(); String captchaValue = captcha.text();
@ -131,7 +138,7 @@ public class AuthorizationController {
captchaValue = captchaValue.split("\\.")[0]; 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) {{ Map<String, Object> imgResult = new HashMap<String, Object>(2) {{
put("img", captcha.toBase64()); put("img", captcha.toBase64());

View File

@ -15,6 +15,9 @@
*/ */
package me.zhengjie.modules.security.security; 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.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -32,6 +35,10 @@ public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override @Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
//当用户在没有授权的情况下访问受保护的REST资源时将调用此方法发送403 Forbidden响应 //当用户在没有授权的情况下访问受保护的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; 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.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
@ -26,14 +29,18 @@ import java.io.IOException;
/** /**
* @author Zheng Jie * @author Zheng Jie
*/ */
@Slf4j
@Component @Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override @Override
public void commence(HttpServletRequest request, public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// 当用户尝试访问安全的REST资源而不提供任何凭据时将调用此方法发送401 响应 // 当用户尝试访问安全的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; package me.zhengjie.modules.security.security;
import lombok.RequiredArgsConstructor; 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.OnlineUserService;
import me.zhengjie.modules.security.service.UserCacheManager; import me.zhengjie.modules.security.service.UserCacheManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
@ -33,11 +33,10 @@ public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFi
private final TokenProvider tokenProvider; private final TokenProvider tokenProvider;
private final SecurityProperties properties; private final SecurityProperties properties;
private final OnlineUserService onlineUserService; private final OnlineUserService onlineUserService;
private final UserCacheManager userCacheManager;
@Override @Override
public void configure(HttpSecurity http) { 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); http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
} }
} }

View File

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

View File

@ -23,7 +23,8 @@ import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j; 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 me.zhengjie.utils.RedisUtils;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@ -42,11 +43,12 @@ import java.util.concurrent.TimeUnit;
@Component @Component
public class TokenProvider implements InitializingBean { public class TokenProvider implements InitializingBean {
private final SecurityProperties properties;
private final RedisUtils redisUtils;
public static final String AUTHORITIES_KEY = "user";
private JwtParser jwtParser; private JwtParser jwtParser;
private JwtBuilder jwtBuilder; 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) { public TokenProvider(SecurityProperties properties, RedisUtils redisUtils) {
this.properties = properties; this.properties = properties;
@ -68,15 +70,19 @@ public class TokenProvider implements InitializingBean {
* Token * Token
* Token Redis * Token Redis
* *
* @param authentication / * @param user /
* @return / * @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 return jwtBuilder
// 加入ID确保生成的 Token 都不一致 .setClaims(claims)
.setId(IdUtil.simpleUUID()) .setSubject(user.getUsername())
.claim(AUTHORITIES_KEY, authentication.getName())
.setSubject(authentication.getName())
.compact(); .compact();
} }

View File

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

View File

@ -16,7 +16,7 @@
package me.zhengjie.modules.security.service; package me.zhengjie.modules.security.service;
import cn.hutool.core.util.RandomUtil; 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.modules.security.service.dto.JwtUserDto;
import me.zhengjie.utils.RedisUtils; import me.zhengjie.utils.RedisUtils;
import me.zhengjie.utils.StringUtils; import me.zhengjie.utils.StringUtils;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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