前言
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。与所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以很容易地进行扩展以满足自定义要求。
引入库
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.cdkjframework</groupId>
<artifactId>cdkj-entity</artifactId>
</dependency>
<dependency>
<groupId>com.cdkjframework</groupId>
<artifactId>cdkj-util</artifactId>
</dependency>
<dependency>
<groupId>com.cdkjframework</groupId>
<artifactId>cdkj-redis</artifactId>
</dependency>
项目结构

受权
身份验证筛选器 AuthenticationFilter
java
package com.cdkjframework.security.authorization;
import com.cdkjframework.constant.BusinessConsts;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.authorization
* @ClassName: AuthenticationFilter
* @Description: java类作用描述
* @Author: xiaLin
* @Version: 1.0
*/
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
/**
* 请求类型
*/
private final List<String> CONTENT_TYPE_LIST = Arrays.asList(MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE);
/**
* 尝试身份验证
*
* @param request 请求
* @param response 响应
* @return 返回结果
* @throws AuthenticationException 权限异常
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//attempt Authentication when Content-Type is json
if (CONTENT_TYPE_LIST.contains(request.getContentType())) {
Object userName = request.getAttribute(BusinessConsts.USER_NAME);
Object password = request.getAttribute(BusinessConsts.PASSWORD);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
return super.attemptAuthentication(request, response);
}
}
}
用户身份验证提供程序 UserAuthenticationProvider
java
package com.cdkjframework.security.authorization;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.RoleEntity;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.security.encrypt.Md5PasswordEncoder;
import com.cdkjframework.security.service.UserRoleService;
import com.cdkjframework.util.tool.number.ConvertUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.authorization
* @ClassName: UserAuthenticationProvider
* @Description: 自定义登录验证
* @Author: xiaLin
* @Version: 1.0
*/
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {
/**
* 用户信息查询服务
*/
private final UserDetailsService userDetailsService;
/**
* 用户角色服务
*/
private final UserRoleService userRoleService;
/**
* 构造函数
*
* @param userDetailsService 用户服务
* @param userRoleService 用户角色服务
*/
public UserAuthenticationProvider(UserDetailsService userDetailsService, UserRoleService userRoleService) {
this.userDetailsService = userDetailsService;
this.userRoleService = userRoleService;
}
/**
* 身份权限验证
*
* @param authentication 身份验证
* @return 返回权限
* @throws AuthenticationException 权限异常
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取表单输入中返回的用户名
String userName = ConvertUtils.convertString(authentication.getPrincipal());
// 获取表单中输入的密码
String password = ConvertUtils.convertString(authentication.getCredentials());
// 查询用户是否存在
Object userDetails = userDetailsService.loadUserByUsername(userName);
if (userDetails == null) {
throw new UsernameNotFoundException("用户名不存在");
}
SecurityUserEntity userInfo = (SecurityUserEntity) userDetails;
// 我们还要判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的
if (!new Md5PasswordEncoder().matches(userInfo.getPassword(), password)) {
throw new BadCredentialsException("用户名或密码不正确");
}
// 还可以加一些其他信息的判断,比如用户账号已停用等判断
if (userInfo.getStatus().equals(IntegerConsts.ZERO) ||
userInfo.getDeleted().equals(IntegerConsts.ONE)) {
throw new LockedException("该用户已被冻结");
}
// 角色集合
Set<GrantedAuthority> authorities = new HashSet<>();
// 查询用户角色
List<RoleEntity> sysRoleEntityList = userRoleService.listRoleByUserId(userInfo.getUserId());
for (RoleEntity sysRoleEntity : sysRoleEntityList) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getRoleName()));
}
userInfo.setRoleList(sysRoleEntityList);
userInfo.setAuthorities(authorities);
// 进行登录
return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
}
/**
* 是否支持权限验证
*/
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
用户权限评估器 UserPermissionEvaluator
typescript
package com.cdkjframework.security.authorization;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.authorization
* @ClassName: UserPermissionEvaluator
* @Description: 自定义权限注解验证
* @Author: xiaLin
* @Version: 1.0
*/
@Component
public class UserPermissionEvaluator implements PermissionEvaluator {
/**
* hasPermission鉴权方法
* 这里仅仅判断PreAuthorize注解中的权限表达式
* 实际中可以根据业务需求设计数据库通过targetUrl和permission做更复杂鉴权
* 当然targetUrl不一定是URL可以是数据Id还可以是管理员标识等,这里根据需求自行设计
*
* @Param authentication 用户身份(在使用hasPermission表达式时Authentication参数默认会自动带上)
* @Param targetUrl 请求路径
* @Param permission 请求路径权限
* @Return boolean 是否通过
*/
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
// // 获取用户信息
// SecurityUserEntity securityUserEntity =(SecurityUserEntity) authentication.getPrincipal();
// // 查询用户权限(这里可以将权限放入缓存中提升效率)
// Set<String> permissions = new HashSet<>();
// List<SysMenuEntity> sysMenuEntityList = sysUserService.selectSysMenuByUserId(securityUserEntity.getUserId());
// for (SysMenuEntity sysMenuEntity:sysMenuEntityList) {
// permissions.add(sysMenuEntity.getPermission());
// }
// // 权限对比
// if (permissions.contains(permission.toString())){
// return true;
// }
return true;
}
/**
* 用于评估权限的替代方法,其中只有目标对象的标识符可用,而不是目标实例本身。
*
* @param authentication 用户身份(在使用hasPermission表达式时Authentication参数默认会自动带上)
* @param targetId 对象实例的标识符(通常为Long)
* @param targetType 表示目标类型的字符串(通常是Java类名)。不为空。
* @param permission 表达式系统提供的权限对象的表示形式。不为空。
* @return 如果权限被授予,则为true,否则为false
*/
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
验证代码筛选器 ValidateCodeFilter
java
package com.cdkjframework.security.authorization;
import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.security.AuthenticationEntity;
import com.cdkjframework.util.log.LogUtils;
import com.cdkjframework.util.network.ResponseUtils;
import com.cdkjframework.util.tool.JsonUtils;
import com.cdkjframework.util.tool.StringUtils;
import com.cdkjframework.util.tool.number.ConvertUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.authorization
* @ClassName: ValidateCodefilter
* @Description: java类作用描述
* @Author: xiaLin
* @Version: 1.0
*/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
/**
* 过虑权限验证
*
* @param request 请求
* @param response 响应
* @param filterChain 过滤链
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String uri = request.getRequestURI();
// 如果为get请求并且请求uri为/login(也就是我们登录表单的form的action地址)
if (uri.contains(BusinessConsts.LOGIN) && !validateCode(request, response)) {
return;
}
filterChain.doFilter(request, response);
}
/**
* 验证用户输入的验证码和session中存的是否一致
*
* @param request 请求
* @param response 响应
*/
private boolean validateCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession httpSession = request.getSession();
String validateValue = ConvertUtils.convertString(httpSession.getAttribute(BusinessConsts.IMAGE_CODE));
//这里需要验证前端传过来的验证码是否和session里面存的一致,并且要判断是否过期
if (StringUtils.isNullAndSpaceOrEmpty(validateValue)) {
return true;
}
// 时间效验
long time = ConvertUtils.convertLong(httpSession.getAttribute(BusinessConsts.TIME));
final long EXPIRATION_TIME = IntegerConsts.FIVE * IntegerConsts.SIXTY * IntegerConsts.ONE_THOUSAND;
if (EXPIRATION_TIME < (System.currentTimeMillis() - time)) {
ResponseUtils.out(response, ResponseBuilder.failBuilder("验证码过期!"));
return false;
}
validateValue = validateValue.toLowerCase();
// 获取数据
String[] values = request.getParameterValues(BusinessConsts.IMAGE_CODE);
if (values == null || values.length == IntegerConsts.ZERO) {
ResponseUtils.out(response, ResponseBuilder.failBuilder("验证码错误!"));
}
String verifyCode = ConvertUtils.convertString(values[IntegerConsts.ZERO]).toLowerCase();
if (StringUtils.isNullAndSpaceOrEmpty(verifyCode) ||
!validateValue.equals(verifyCode)) {
ResponseUtils.out(response, ResponseBuilder.failBuilder("验证码错误!"));
return false;
}
return true;
}
}
配置
安全配置 SecurityConfigure
scss
package com.cdkjframework.security.configure;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.security.authorization.AuthenticationFilter;
import com.cdkjframework.security.authorization.UserAuthenticationProvider;
import com.cdkjframework.security.authorization.UserPermissionEvaluator;
import com.cdkjframework.security.authorization.ValidateCodeFilter;
import com.cdkjframework.security.encrypt.JwtAuthenticationFilter;
import com.cdkjframework.security.encrypt.Md5PasswordEncoder;
import com.cdkjframework.security.handler.*;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.config
* @ClassName: SecurityConfig
* @Description: 权限配置 开启权限注解,默认是关闭的
* @Author: xiaLin
* @Version: 1.0
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfigure {
/**
* 放行路径
*/
private final String[] PATTERNS = new String[]{"/**"};
/**
* 自定义登录成功处理器
*/
private final UserLoginSuccessHandler userLoginSuccessHandler;
/**
* 自定义登录失败处理器
*/
private final UserLoginFailureHandler userLoginFailureHandler;
/**
* 自定义注销成功处理器
*/
private final UserLogoutSuccessHandler userLogoutSuccessHandler;
/**
* 自定义暂无权限处理器
*/
private final UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
/**
* 自定义未登录的处理器
*/
private final UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
/**
* 自定义登录逻辑验证器
*/
private final UserAuthenticationProvider userAuthenticationProvider;
/**
* 读取配置文件
*/
private final CustomConfig customConfig;
/**
* 身份验证管理器
*/
@Resource(name = "authentication")
private AuthenticationManager authentication;
/**
* 鉴权管理类
*/
@Bean(name = "authentication")
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* 加密方式
*/
@Bean
public Md5PasswordEncoder md5PasswordEncoder() {
return new Md5PasswordEncoder();
}
/**
* 注入自定义PermissionEvaluator
*/
@Bean
public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(new UserPermissionEvaluator());
return handler;
}
/**
* Spring Security 过滤链
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
int size = customConfig.getPatternsUrls().size();
String[] patternsUrls = customConfig.getPatternsUrls().toArray(new String[size]);
return http
// 配置未登录自定义处理类
.httpBasic(basic ->
basic.authenticationEntryPoint(userAuthenticationEntryPointHandler)
)
// 禁用缓存
.headers(header -> header.cacheControl(cache -> cache.disable()))
// 关闭csrf
.csrf(AbstractHttpConfigurer::disable)
// 配置登录地址
.formLogin(form -> form.loginPage(customConfig.getLoginPage())
.loginProcessingUrl(customConfig.getLoginUrl())
.defaultSuccessUrl(customConfig.getLoginSuccess())
// 配置登录成功自定义处理类
.successHandler(userLoginSuccessHandler)
// 配置登录失败自定义处理类
.failureHandler(userLoginFailureHandler)
.permitAll()
)
// 禁用默认登出页
.logout(logout -> logout.logoutUrl(customConfig.getLogoutUrl())
.logoutSuccessHandler(userLogoutSuccessHandler)
.permitAll()
)
// 配置没有权限自定义处理类
.exceptionHandling(exception -> exception.accessDeniedHandler(userAuthAccessDeniedHandler))
// 禁用session
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置拦截信息
.authorizeHttpRequests(authorization -> authorization
// 允许所有的OPTIONS请求
.requestMatchers(HttpMethod.OPTIONS, PATTERNS).permitAll()
// 放行白名单
.requestMatchers(patternsUrls).permitAll()
// 根据接口所需权限进行动态鉴权
.anyRequest()
.authenticated()
)
// 配置登录验证逻辑
.authenticationProvider(userAuthenticationProvider)
// 注册自定义拦截器
.addFilterBefore(new ValidateCodeFilter(), UsernamePasswordAuthenticationFilter.class)
// 权限验证
.addFilterAt(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 添加JWT过滤器
.addFilter(new JwtAuthenticationFilter(authentication, customConfig))
.build();
}
/**
* 身份验证筛选器
*
* @return 返回 身份验证筛选器
* @throws Exception 异常信息
*/
private AuthenticationFilter authenticationFilter() throws Exception {
AuthenticationFilter filter = new AuthenticationFilter();
filter.setAuthenticationManager(authentication);
filter.setFilterProcessesUrl(customConfig.getLoginUrl());
// 处理登录成功
filter.setAuthenticationSuccessHandler(userLoginSuccessHandler);
// 处理登录失败
filter.setAuthenticationFailureHandler(userLoginFailureHandler);
return filter;
}
}
接口
安全认证控制器 SecurityCertificateController
该接口类提供了APP扫码登录、票据认证、刷新票据、刷新TOKEN等接口
ini
package com.cdkjframework.security.controller;
import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.exceptions.GlobalException;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.security.service.UserAuthenticationService;
import com.cdkjframework.util.encrypts.AesUtils;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.files.images.code.QrCodeUtils;
import com.cdkjframework.util.make.VerifyCodeUtils;
import com.cdkjframework.util.network.http.HttpRequestUtils;
import com.cdkjframework.util.tool.StringUtils;
import com.cdkjframework.util.tool.number.ConvertUtils;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import static com.cdkjframework.constant.BusinessConsts.TICKET_SUFFIX;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.controller
* @ClassName: SecurityCertificateController
* @Description: 安全认证接口
* @Author: xiaLin
* @Version: 1.0
*/
@RestController
@Tag(name = "安全认证接口")
@RequiredArgsConstructor
@RequestMapping(value = "/security")
public class SecurityCertificateController {
/**
* 二维生成
*/
private final QrCodeUtils qrCodeUtils;
/**
* 配置文件
*/
private final CustomConfig customConfig;
/**
* 用户权限服务
*/
private final UserAuthenticationService userAuthenticationServiceImpl;
/**
* 获取验证码
*
* @param request 请求
* @param response 响应
* @throws IOException IO异常信息
*/
@GetMapping(value = "/verify/code")
@Operation(summary = "获取验证码")
public void verificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
OutputStream outputStream = response.getOutputStream();
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE);
// 创建 session
HttpSession session = request.getSession();
// 生成验证码
String code = VerifyCodeUtils.outputVerifyImage(IntegerConsts.ONE_HUNDRED, IntegerConsts.THIRTY_FIVE,
outputStream, IntegerConsts.FOUR);
// 将图形验证码存入到session中
session.setAttribute(BusinessConsts.IMAGE_CODE, code);
session.setAttribute(BusinessConsts.TIME, System.currentTimeMillis());
}
/**
* 获取扫码二维码
*
* @param request 请求
* @param response 响应
* @throws IOException IO异常信息
*/
@Operation(summary = "获取验证码")
@GetMapping(value = "/scan/qrcode.html")
public void scanCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
OutputStream outputStream = response.getOutputStream();
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML_VALUE);
// 创建 session
HttpSession session = request.getSession();
String id = session.getId().toLowerCase();
long currentTime = System.currentTimeMillis();
StringBuilder content = new StringBuilder(id);
content.append(StringUtils.COMMA);
content.append(currentTime);
// 生成二维码
if (StringUtils.isNotNullAndEmpty(customConfig.getQrlogo())) {
// 添加LOGO
InputStream stream = HttpRequestUtils.readImages(customConfig.getQrlogo());
qrCodeUtils.createQrCode(content.toString(), stream, outputStream);
} else {
qrCodeUtils.createQrCode(content.toString(), outputStream);
}
String statusKey = CacheConsts.USER_PREFIX + BusinessConsts.STATUS;
String timeKey = CacheConsts.USER_PREFIX + BusinessConsts.TIME;
// 将图形验证码存入到session中
RedisUtils.hSet(timeKey, id, String.valueOf(currentTime));
RedisUtils.hSet(statusKey, id, StringUtils.ZERO);
}
/**
* 验证二维码是否已被扫码
*
* @param request 请求
* @param response 响应
* @throws IOException IO异常信息
*/
@ResponseBody
@Operation(summary = "验证二维码是否已被扫码")
@GetMapping(value = "/validate.html")
public ResponseBuilder validate(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 创建 session
HttpSession session = request.getSession();
String id = session.getId().toLowerCase();
String statusKey = CacheConsts.USER_PREFIX + BusinessConsts.STATUS;
String timeKey = CacheConsts.USER_PREFIX + BusinessConsts.TIME;
// 读取状态
int status = ConvertUtils.convertInt(RedisUtils.hGet(statusKey, id));
long time = ConvertUtils.convertLong(RedisUtils.hGet(timeKey, id));
// 返回结果
String message;
switch (status) {
case 1 -> {
long currentTime = System.currentTimeMillis();
long value = ((currentTime - time) / IntegerConsts.ONE_THOUSAND) / IntegerConsts.SIXTY;
message = "success";
if (value > IntegerConsts.FIVE) {
message = "timeout";
}
}
case 2 -> {
String tokenKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + id;
String token = RedisUtils.syncGet(tokenKey);
if (StringUtils.isNotNullAndEmpty(token)) {
message = "successful";
Claims claims = JwtUtils.parseJwt(token, customConfig.getJwtKey());
if (claims == null) {
message = "timeout";
} else { // 生成 ticket 票据
token = ConvertUtils.convertString(claims.get(BusinessConsts.HEADER_TOKEN));
String ticket = URLEncoder.encode(AesUtils.base64Encode(token), StandardCharsets.UTF_8) + TICKET_SUFFIX;
response.setHeader(BusinessConsts.TICKET, ticket);
String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN;
RedisUtils.hSet(ticketKey, token, ticket);
}
} else {
message = "timeout";
}
}
default -> message = StringUtils.Empty;
}
// 返回结果
return ResponseBuilder.successBuilder(message);
}
/**
* 票据认证
*
* @throws IOException IO异常信息
*/
@ResponseBody
@Operation(summary = "票据认证")
@GetMapping(value = "/ticket.html")
public SecurityUserEntity ticket(@RequestParam("ticket") String ticket, HttpServletResponse response) throws Exception {
return userAuthenticationServiceImpl.ticket(ticket, response);
}
/**
* 票据刷新
*
* @param request 响应
* @return 返回票据信息
* @throws UnsupportedEncodingException 异常信息
* @throws GlobalException 异常信息
*/
@ResponseBody
@Operation(summary = "票据刷新")
@GetMapping(value = "/refresh/ticket.html")
public ResponseBuilder refreshTicket(HttpServletRequest request) throws UnsupportedEncodingException, GlobalException {
String ticket = userAuthenticationServiceImpl.refreshTicket(request);
// 返回结果
return ResponseBuilder.successBuilder(ticket);
}
/**
* token 刷新
*
* @param request 请求
* @param response 响应
* @return 返回票据信息
* @throws UnsupportedEncodingException 异常信息
* @throws GlobalException 异常信息
*/
@ResponseBody
@Operation(summary = "token 刷新")
@GetMapping(value = "/refresh/token.html")
public ResponseBuilder refreshToken(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException, GlobalException {
String ticket = userAuthenticationServiceImpl.refreshToken(request, response);
// 返回结果
return ResponseBuilder.successBuilder(ticket);
}
/**
* 扫码确认接口
*/
@ResponseBody
@Operation(summary = "验证二维码是否已被扫码")
@GetMapping(value = "/confirm.html")
public void confirm(@RequestParam("id") String id) {
String statusKey = CacheConsts.USER_PREFIX + BusinessConsts.STATUS;
RedisUtils.hSet(statusKey, id, String.valueOf(IntegerConsts.ONE));
}
/**
* 扫码完成接口
*
* @param username 用户名
* @param id ID
* @throws IOException IO异常信息
*/
@ResponseBody
@Operation(summary = "扫码完成接口【即登录】")
@PostMapping(value = "/completed.html")
public void completed(@RequestParam("username") String username, @RequestParam("id") String id) throws Exception {
String statusKey = CacheConsts.USER_PREFIX + BusinessConsts.STATUS;
Integer status = ConvertUtils.convertInt(RedisUtils.hGet(statusKey, id));
if (!status.equals(IntegerConsts.ONE)) {
throw new GlobalException("二维码已过期,请刷新重试!");
}
RedisUtils.syncDel(statusKey);
// 受权信息
userAuthenticationServiceImpl.authenticate(username, id);
RedisUtils.hSet(statusKey, id, String.valueOf(IntegerConsts.TWO));
}
/**
* 用户退出登录
*
* @param request 响应
* @throws GlobalException 异常信息
*/
@ResponseBody
@Operation(summary = "扫码完成接口【即登录】")
@PostMapping(value = "/logout.html")
public void logout(HttpServletRequest request) throws GlobalException {
userAuthenticationServiceImpl.logout(request);
}
}
加密
Jwt身份验证筛选器 JwtAuthenticationFilter
java
package com.cdkjframework.security.encrypt;
import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.log.LogUtils;
import com.cdkjframework.util.tool.JsonUtils;
import com.cdkjframework.util.tool.StringUtils;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.CollectionUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.server.jwt
* @ClassName: AuthenticationFilter
* @Description: java类作用描述
* @Author: xiaLin
* @Version: 1.0
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
/**
* 日志
*/
private LogUtils logUtils = LogUtils.getLogger(JwtAuthenticationFilter.class);
/**
* 配置读取
*/
private CustomConfig customConfig;
/**
* 替换字符
*/
private final String REPLACE = "**";
/**
* 构造函数
*
* @param authenticationManager 身份验证管理器
*/
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, CustomConfig customConfig) {
super(authenticationManager);
this.customConfig = customConfig;
}
/**
* 权限验证过虑
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
try {
// 是否验证权限
if (!verifyToken(request)) {
chain.doFilter(request, response);
return;
}
// 请求体的头中是否包含Authorization
String token = request.getHeader(BusinessConsts.HEADER_TOKEN);
// Authorization中是否包含Bearer,有一个不包含时直接返回
if (StringUtils.isNullAndSpaceOrEmpty(token)) {
responseJson(response);
chain.doFilter(request, response);
return;
}
// 获取权限失败,会抛出异常
UsernamePasswordAuthenticationToken authentication = getAuthentication(token);
// 获取后,将Authentication写入SecurityContextHolder中供后序使用
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (Exception e) {
responseJson(response);
}
}
/**
* 未登錄時的提示
*
* @param response 响应
*/
private void responseJson(HttpServletResponse response) {
try {
// 未登錄時,使用json進行提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
ResponseBuilder builder = ResponseBuilder.failBuilder();
builder.setCode(HttpServletResponse.SC_FORBIDDEN);
builder.setMessage("请登录!");
out.write(JsonUtils.objectToJsonString(builder));
out.flush();
out.close();
} catch (Exception e) {
logUtils.error(e);
}
}
/**
* 通过 token,获取用户信息
*
* @param token token 值
* @return 返回用户权限
*/
private UsernamePasswordAuthenticationToken getAuthentication(String token) {
if (StringUtils.isNotNullAndEmpty(token)) {
// 通过 token 解析出用户信息
Claims claims = JwtUtils.parseJwt(token, customConfig.getJwtKey());
Object username = claims.get(BusinessConsts.USER_NAME);
if (StringUtils.isNullAndSpaceOrEmpty(username)) {
username = claims.get(BusinessConsts.LOGIN_NAME);
}
// 不为 null,返回
if (StringUtils.isNotNullAndEmpty(username)) {
return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
}
return null;
}
return null;
}
/**
* 是否需要验证 token
*
* @param request 请求
* @return 结果
*/
private boolean verifyToken(HttpServletRequest request) {
// 读取配置
List<String> patternsUrls = customConfig.getPatternsUrls();
// 请求地址
String contextPath = request.getServletPath();
// 比对结构
List<String> filterList = patternsUrls.stream()
.filter(url -> contextPath.startsWith(url.replace(REPLACE, StringUtils.Empty)))
.collect(Collectors.toList());
// 返回结果
return CollectionUtils.isEmpty(filterList);
}
}
Md5密码编码器 Md5PasswordEncoder
typescript
package com.cdkjframework.security.encrypt;
import com.cdkjframework.util.encrypts.Md5Utils;
import com.cdkjframework.util.tool.StringUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.encrypt
* @ClassName: Md5PasswordEncoder
* @Description: java类作用描述
* @Author: xiaLin
* @Version: 1.0
*/
public class Md5PasswordEncoder implements PasswordEncoder {
/**
* 对原始密码进行编码
*
* @param rawPassword 密码
*/
@Override
public String encode(CharSequence rawPassword) {
return Md5Utils.getMd5(rawPassword.toString());
}
/**
* 比较
*
* @param rawPassword 要编码和匹配的原始密码
* @param encodedPassword 要与之比较的存储器中的编码密码
* @return 如果原始密码在编码后与存储中的编码密码匹配,则为true
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
// 密码是否为空
if (StringUtils.isNullAndSpaceOrEmpty(encodedPassword)) {
return false;
}
// 对密码加密
encodedPassword = encode(encodedPassword);
// 返回比较结果
return rawPassword.toString().equals(encodedPassword);
}
}
结束处理
用户身份验证访问被拒绝处理程序 UserAuthAccessDeniedHandler
java
package com.cdkjframework.security.handler;
import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.util.network.ResponseUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ProjectName: cdkjframework-cloud
* @Package: com.cdkjframework.cloud.handler
* @ClassName: UserAuthAccessDeniedHandler
* @Description: java类作用描述
* @Author: xiaLin
* @Version: 1.0
*/
@Component
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler {
/**
* 无权限返回结果
*
* @param request 请求
* @param response 响应
* @param e 权限异常
* @throws IOException 异常信息
* @throws ServletException 异常信息
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
ResponseBuilder builder = ResponseBuilder.failBuilder("未授权");
ResponseUtils.out(response, builder);
}
}
用户身份验证入口点处理程序 UserAuthenticationEntryPointHandler
java
package com.cdkjframework.security.handler;
import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.util.network.ResponseUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @ProjectName: cdkjframework-cloud
* @Package: com.cdkjframework.cloud.handler
* @ClassName: UserAuthenticationEntryPointHandler
* @Description: java类作用描述
* @Author: xiaLin
* @Version: 1.0
*/
@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
/**
* 用户未登录返回结果
*
* @param request 请求
* @param response 响应
* @param e 权限异常
* @throws IOException 异常信息
* @throws ServletException 异常信息
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
ResponseBuilder builder = ResponseBuilder.failBuilder("登录错误,请稍后在试!");
ResponseUtils.out(response, builder);
}
}
用户登录失败处理程序 UserLoginFailureHandler
java
package com.cdkjframework.security.handler;
import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.util.log.LogUtils;
import com.cdkjframework.util.network.ResponseUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ProjectName: cdkjframework-cloud
* @Package: com.cdkjframework.cloud.handler
* @ClassName: UserLoginFailureHandler
* @Description: java类作用描述
* @Author: xiaLin
* @Version: 1.0
*/
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
/**
* 日志
*/
private LogUtils logUtils = LogUtils.getLogger(UserLoginFailureHandler.class);
/**
* 登录失败返回结果
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
ResponseBuilder builder;
// 这些对于操作的处理类可以根据不同异常进行不同处理
if (exception instanceof UsernameNotFoundException) {
logUtils.error("【登录失败】" + exception.getMessage());
builder = ResponseBuilder.failBuilder(exception.getMessage());
} else if (exception instanceof LockedException) {
logUtils.error("【登录失败】" + exception.getMessage());
builder = ResponseBuilder.failBuilder(exception.getMessage());
} else if (exception instanceof BadCredentialsException) {
logUtils.error("【登录失败】" + exception.getMessage());
builder = ResponseBuilder.failBuilder(exception.getMessage());
} else {
builder = ResponseBuilder.failBuilder("用户名或密码不正确");
}
ResponseUtils.out(response, builder);
}
}
用户登录成功处理程序 UserLoginSuccessHandler
ini
package com.cdkjframework.security.handler;
import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.BmsConfigureEntity;
import com.cdkjframework.entity.user.ResourceEntity;
import com.cdkjframework.entity.user.RoleEntity;
import com.cdkjframework.entity.user.WorkflowEntity;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.security.service.ConfigureService;
import com.cdkjframework.security.service.ResourceService;
import com.cdkjframework.security.service.WorkflowService;
import com.cdkjframework.util.encrypts.AesUtils;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.encrypts.Md5Utils;
import com.cdkjframework.util.network.ResponseUtils;
import com.cdkjframework.util.tool.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.cdkjframework.constant.BusinessConsts.TICKET_SUFFIX;
/**
* @ProjectName: cdkjframework-cloud
* @Package: com.cdkjframework.cloud.handler
* @ClassName: UserLoginSuccessHandler
* @Description: 用户登录成功
* @Author: xiaLin
* @Version: 1.0
*/
@Component
@RequiredArgsConstructor
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
/**
* 有效时间
*/
private final long EFFECTIVE = IntegerConsts.TWENTY_FOUR * IntegerConsts.SIXTY * IntegerConsts.SIXTY;
/**
* 自定义配置
*/
private final CustomConfig customConfig;
/**
* 配置服务
*/
private final ConfigureService configureServiceImpl;
/**
* 资源服务
*/
private final ResourceService resourceServiceImpl;
/**
* 工作流服务
*/
private final WorkflowService workflowServiceImpl;
/**
* 权限认证成功
*
* @param request 请求
* @param response 响应
* @param authentication 权限
* @throws IOException 异常信息
* @throws ServletException 异常信息
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
SecurityUserEntity user = (SecurityUserEntity) authentication.getPrincipal();
ResponseBuilder builder = ResponseBuilder.successBuilder();
builder.setData(user);
// 构建 token
buildJwtToken(request, response, user);
// 获取配置信息
BmsConfigureEntity configure = new BmsConfigureEntity();
configure.setOrganizationId(user.getOrganizationId());
configure.setTopOrganizationId(user.getTopOrganizationId());
List<BmsConfigureEntity> configureList = configureServiceImpl.listConfigure(configure);
user.setConfigureList(configureList);
// 用户角色
List<RoleEntity> roleList = user.getRoleList();
if (!CollectionUtils.isEmpty(roleList)) {
// 用户资源
List<ResourceEntity> resourceList = resourceServiceImpl.listResource(roleList, user.getUserId());
// 用户资源写入缓存
String key = CacheConsts.USER_RESOURCE + user.getUserId();
RedisUtils.syncEntitySet(key, resourceList, EFFECTIVE);
user.setResourceList(resourceList);
}
// 查询工作流信息
WorkflowEntity workflow = new WorkflowEntity();
workflow.setOrganizationId(user.getOrganizationId());
workflow.setTopOrganizationId(user.getTopOrganizationId());
workflowServiceImpl.listWorkflow(workflow);
// 返回登录结果
ResponseUtils.out(response, builder);
}
/**
* 生成 jwt token
*
* @param user 用户实体
* @param request 请求
* @param response 响应
*/
private void buildJwtToken(HttpServletRequest request, HttpServletResponse response, SecurityUserEntity user) throws UnsupportedEncodingException {
// 生成 JWT token
Map<String, Object> map = new HashMap<>(IntegerConsts.FOUR);
map.put(BusinessConsts.LOGIN_NAME, user.getUsername());
long time = System.currentTimeMillis() / IntegerConsts.ONE_THOUSAND;
map.put(BusinessConsts.TIME, time);
map.put(BusinessConsts.USER_NAME, user.getUsername());
map.put(BusinessConsts.USER_TYPE, user.getUserType());
map.put(BusinessConsts.DISPLAY_NAME, user.getDisplayName());
// 暂不需要该参数
String userAgent = StringUtils.Empty;
StringBuilder builder = new StringBuilder();
/**
* 加密 token 参数
*/
String TOKEN_ENCRYPTION = "loginName=%s&effective=%s&time=%s&userAgent=%s";
builder.append(String.format(TOKEN_ENCRYPTION,
user.getUsername(), EFFECTIVE, time, userAgent));
String token = Md5Utils.getMd5(builder.toString());
map.put(BusinessConsts.HEADER_TOKEN, token);
String jwtToken = JwtUtils.createJwt(map, customConfig.getJwtKey());
response.setHeader(BusinessConsts.HEADER_TOKEN, jwtToken);
// 票据添加到响应中
String ticket = URLEncoder.encode(AesUtils.base64Encode(token), StandardCharsets.UTF_8.toString()) + TICKET_SUFFIX;
response.setHeader(BusinessConsts.TICKET, ticket);
// 票据 token 关系
String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + token;
RedisUtils.syncSet(ticketKey, jwtToken, IntegerConsts.SIXTY);
// 用户信息写入缓存
String key = CacheConsts.USER_LOGIN + token;
RedisUtils.syncEntitySet(key, user, EFFECTIVE);
}
}
用户注销成功处理程序 UserLogoutSuccessHandler
ini
package com.cdkjframework.security.handler;
import com.alibaba.fastjson.JSONObject;
import com.cdkjframework.builder.ResponseBuilder;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.network.ResponseUtils;
import com.cdkjframework.util.tool.JsonUtils;
import com.cdkjframework.util.tool.StringUtils;
import com.cdkjframework.util.tool.number.ConvertUtils;
import io.jsonwebtoken.Claims;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* @ProjectName: cdkjframework-cloud
* @Package: com.cdkjframework.cloud.handler
* @ClassName: LogoutSuccessHandler
* @Description: 退出登录成功
* @Author: xiaLin
* @Version: 1.0
*/
@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {
/**
* 受权
*/
private static final String TOKEN = "token";
/**
* 用户登出返回结果
* 这里应该让前端清除掉Token
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader(TOKEN);
Claims claims = JwtUtils.parseJwt(token, new CustomConfig().getJwtKey());
if (claims != null) {
String tokenKey = ConvertUtils.convertString(claims.get(TOKEN));
final String userKey = CacheConsts.USER_LOGIN + tokenKey;
// 读取用户
String userInfo = RedisUtils.syncGet(userKey);
if (StringUtils.isNotNullAndEmpty(userInfo)) {
JSONObject object = JsonUtils.parseObject(userInfo);
/**
* 用户ID
*/
String ID = "id";
String userId = object.getString(ID);
final String resourceKey = CacheConsts.USER_RESOURCE + userId;
RedisUtils.syncDel(resourceKey);
}
RedisUtils.syncDel(userKey);
}
ResponseBuilder builder = ResponseBuilder.successBuilder();
SecurityContextHolder.clearContext();
ResponseUtils.out(response, builder);
}
}
服务或接口
抽象用户详细信息服务 AbstractUserDetailsService
该抽象类需要应用服务端实现
java
package com.cdkjframework.security.service.impl;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.service
* @ClassName: AbstractUserDetailsService
* @Description: java类作用描述
* @Author: xiaLin
* @Version: 1.0
*/
@Component
public abstract class AbstractUserDetailsService implements UserDetailsService {
/**
* 查询用户信息
*
* @param username 用户名
* @return 返回用户信息
* @throws UsernameNotFoundException 用户未找到异常信息
*/
@Override
public abstract SecurityUserEntity loadUserByUsername(String username) throws UsernameNotFoundException;
}
用户身份验证服务
接口 UserAuthenticationService
java
package com.cdkjframework.security.service;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.exceptions.GlobalException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.service
* @ClassName: UserAuthenticationService
* @Description: java类作用描述
* @Author: xiaLin
* @Date: 2023/5/16 22:41
* @Version: 1.0
*/
public interface UserAuthenticationService {
/**
* 获取登录参数
*/
String GRANT_TYPE = "grantType";
/**
* 有效时间
*/
long EFFECTIVE = IntegerConsts.TWENTY_FOUR * IntegerConsts.SIXTY * IntegerConsts.SIXTY;
/**
* 授权常量
*/
String AUTHORIZATION = "token";
/**
* 身份权限验证
*
* @param userName 用户名
* @param sessionId 会话id
* @return 返回权限
* @throws AuthenticationException 权限异常
* @throws ServletException 权限异常
* @throws IOException 权限异常
*/
Authentication authenticate(String userName, String sessionId) throws AuthenticationException, IOException, ServletException;
/**
* 票据认证
*
* @param ticket 票据
* @param response 响应
* @return 返回用户信息
* @throws Exception IO异常信息
*/
SecurityUserEntity ticket(String ticket, HttpServletResponse response) throws Exception;
/**
* 刷新 token
*
* @param request 响应
* @return 返回票据
* @throws GlobalException 异常信息
* @throws UnsupportedEncodingException 异常信息
*/
String refreshTicket(HttpServletRequest request) throws GlobalException, UnsupportedEncodingException;
/**
* token 刷新
*
* @param request 请求
* @param response 响应
* @return 返回最新 token
* @throws GlobalException 异常信息
* @throws UnsupportedEncodingException 异常信息
*/
String refreshToken(HttpServletRequest request, HttpServletResponse response) throws GlobalException, UnsupportedEncodingException;
/**
* 用户退出登录
*
* @param request 响应
* @throws GlobalException 异常信息
*/
void logout(HttpServletRequest request) throws GlobalException;
}
实现 UserAuthenticationServiceImpl
ini
package com.cdkjframework.security.service.impl;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.BmsConfigureEntity;
import com.cdkjframework.entity.user.ResourceEntity;
import com.cdkjframework.entity.user.RoleEntity;
import com.cdkjframework.entity.user.UserEntity;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.exceptions.GlobalException;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.security.service.*;
import com.cdkjframework.util.encrypts.AesUtils;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.encrypts.Md5Utils;
import com.cdkjframework.util.tool.JsonUtils;
import com.cdkjframework.util.tool.StringUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import static com.cdkjframework.constant.BusinessConsts.TICKET_SUFFIX;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.authorization
* @ClassName: UserAuthenticationProvider
* @Description: 自定义登录验证
* @Author: xiaLin
* @Version: 1.0
*/
@Component
@RequiredArgsConstructor
public class UserAuthenticationServiceImpl implements UserAuthenticationService {
/**
* 用户登录成功服务
*/
private final UserLoginSuccessService userLoginSuccessServiceImpl;
/**
* 用户信息查询服务
*/
private final UserDetailsService userDetailsService;
/**
* 用户角色服务
*/
private final UserRoleService userRoleService;
/**
* 配置信息
*/
private final CustomConfig customConfig;
/**
* 资源服务
*/
private final ResourceService resourceServiceImpl;
/**
* 配置服务
*/
private final ConfigureService configureServiceImpl;
/**
* 身份权限验证
*
* @param userName 用户名
* @param sessionId 会话id
* @return 返回权限
* @throws AuthenticationException 权限异常
*/
@Override
public Authentication authenticate(String userName, String sessionId) throws AuthenticationException, IOException, ServletException {
// 查询用户是否存在
Object userDetails = userDetailsService.loadUserByUsername(userName);
if (userDetails == null) {
throw new UsernameNotFoundException("用户名不存在");
}
SecurityUserEntity userInfo = (SecurityUserEntity) userDetails;
// 还可以加一些其他信息的判断,比如用户账号已停用等判断
if (userInfo.getStatus().equals(IntegerConsts.ZERO) ||
userInfo.getDeleted().equals(IntegerConsts.ONE)) {
throw new LockedException("该用户已被冻结");
}
// 角色集合
Set<GrantedAuthority> authorities = new HashSet<>();
// 查询用户角色
List<RoleEntity> sysRoleEntityList = userRoleService.listRoleByUserId(userInfo.getUserId());
for (RoleEntity sysRoleEntity : sysRoleEntityList) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getRoleName()));
}
userInfo.setAuthorities(authorities);
// 进行登录
Authentication authentication = new UsernamePasswordAuthenticationToken(userInfo, userInfo.getPassword(), authorities);
// 登录成功后操作
userLoginSuccessServiceImpl.onAuthenticationSuccess(sessionId, authentication);
// 返回结果
return authentication;
}
/**
* 票据认证
*
* @param ticket 票据
* @param response 响应
* @throws IOException IO异常信息
*/
@Override
public SecurityUserEntity ticket(String ticket, HttpServletResponse response) throws Exception {
if (StringUtils.isNullAndSpaceOrEmpty(ticket)) {
throw new GlobalException("票据错误!");
}
ticket = URLDecoder.decode(ticket, StandardCharsets.UTF_8.toString());
String token = AesUtils.base64Decrypt(ticket
.replace(BusinessConsts.TICKET_SUFFIX, StringUtils.Empty));
// 读取用户信息
String key = CacheConsts.USER_LOGIN + token;
SecurityUserEntity user = RedisUtils.syncGetEntity(key, SecurityUserEntity.class);
if (user == null) {
throw new GlobalException("用户信息错误!");
}
// 票据 token 关系
String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + token,
jwtToken = RedisUtils.syncGet(ticketKey),
// 资源 key
resourceKey;
RedisUtils.syncDel(ticketKey);
user.setToken(jwtToken);
response.setHeader(BusinessConsts.HEADER_TOKEN, jwtToken);
// 读取当前用户所登录平台资源数据
List<ResourceEntity> resourceList = resourceServiceImpl.listResource(new ArrayList<>(), user.getUserId());
user.setResourceList(resourceList);
// 读取配置信息
BmsConfigureEntity configure = new BmsConfigureEntity();
configure.setOrganizationId(user.getCurrentOrganizationId());
configure.setTopOrganizationId(user.getTopOrganizationId());
configure.setDeleted(IntegerConsts.ZERO);
configure.setStatus(IntegerConsts.ONE);
List<BmsConfigureEntity> configureList = configureServiceImpl.listConfigure(configure);
user.setConfigureList(configureList);
// 删除数据
RedisUtils.syncDel(ticketKey);
user.setPassword(StringUtils.Empty);
// 返回结果
return user;
}
/**
* 刷新票据
*
* @param request 响应
* @return 返回票据
*/
@Override
public String refreshTicket(HttpServletRequest request) throws GlobalException, UnsupportedEncodingException {
String jwtToken = request.getHeader(AUTHORIZATION);
// 验证TOKEN有效性
String tokenValue = JwtUtils.checkToken(jwtToken, customConfig.getJwtKey(), StringUtils.Empty);
// 票据 token 关系
String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + tokenValue;
// 存储票据信息
RedisUtils.syncSet(ticketKey, jwtToken, IntegerConsts.SIXTY);
// 返回票据
return URLEncoder.encode(AesUtils.base64Encode(tokenValue), StandardCharsets.UTF_8.toString()) + TICKET_SUFFIX;
}
/**
* token 刷新
*
* @param request 请求
* @param response 响应
* @return 返回最新 token
* @throws GlobalException 异常信息
* @throws UnsupportedEncodingException 异常信息
*/
@Override
public String refreshToken(HttpServletRequest request, HttpServletResponse response) throws GlobalException, UnsupportedEncodingException {
String jwtToken = request.getHeader(AUTHORIZATION);
// 验证 TOKEN 有效性
String tokenValue = JwtUtils.checkToken(jwtToken, customConfig.getJwtKey(), StringUtils.Empty);
if (StringUtils.isNotNullAndEmpty(tokenValue)) {
// 用户信息
String key = CacheConsts.USER_LOGIN + tokenValue;
SecurityUserEntity user = RedisUtils.syncGetEntity(key, SecurityUserEntity.class);
buildJwtToken(user, response);
}
return null;
}
/**
* 用户退出登录
*
* @param request 响应
* @throws GlobalException 异常信息
*/
@Override
public void logout(HttpServletRequest request) throws GlobalException {
String jwtToken = request.getHeader(AUTHORIZATION);
// 验证TOKEN有效性
String tokenValue = JwtUtils.checkToken(jwtToken, customConfig.getJwtKey(), StringUtils.Empty);
// 先读取用户信息
String key = CacheConsts.USER_LOGIN + tokenValue;
String jsonCache = RedisUtils.syncGet(key);
if (StringUtils.isNullAndSpaceOrEmpty(jsonCache)) {
return;
}
UserEntity user = JsonUtils.jsonStringToBean(jsonCache, UserEntity.class);
// 删除 用户信息
RedisUtils.syncDel(key);
// 删除 资源
key = CacheConsts.USER_RESOURCE + tokenValue;
RedisUtils.syncDel(key);
// 删除 用户全部资源
key = CacheConsts.USER_RESOURCE_ALL + user.getId();
RedisUtils.syncDel(key);
// 删除 用户工作流引擎
key = CacheConsts.WORK_FLOW + user.getId();
RedisUtils.syncDel(key);
}
/**
* 生成 jwt token
*
* @param user 用户实体
* @param response 响应
*/
private void buildJwtToken(SecurityUserEntity user, HttpServletResponse response) throws UnsupportedEncodingException {
// 生成 JWT token
Map<String, Object> map = new HashMap<>(IntegerConsts.FOUR);
map.put(BusinessConsts.LOGIN_NAME, user.getUsername());
long time = System.currentTimeMillis() / IntegerConsts.ONE_THOUSAND;
map.put(BusinessConsts.TIME, time);
map.put(BusinessConsts.USER_NAME, user.getUsername());
map.put(BusinessConsts.USER_TYPE, user.getUserType());
map.put(BusinessConsts.DISPLAY_NAME, user.getDisplayName());
// 暂不需要该参数
String userAgent = StringUtils.Empty;
StringBuilder builder = new StringBuilder();
// 加密 token 参数
String TOKEN_ENCRYPTION = "loginName=%s&effective=%s&time=%s&userAgent=%s";
builder.append(String.format(TOKEN_ENCRYPTION,
user.getUsername(), EFFECTIVE, time, userAgent));
String token = Md5Utils.getMd5(builder.toString());
map.put(BusinessConsts.HEADER_TOKEN, token);
String jwtToken = JwtUtils.createJwt(map, customConfig.getJwtKey());
response.setHeader(BusinessConsts.HEADER_TOKEN, jwtToken);
// 用户信息写入缓存
String key = CacheConsts.USER_LOGIN + token;
RedisUtils.syncEntitySet(key, user, EFFECTIVE);
}
}
用户登录成功服务
接口 UserLoginSuccessService
java
package com.cdkjframework.security.service;
import org.springframework.security.core.Authentication;
import jakarta.servlet.ServletException;
import java.io.IOException;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.service
* @ClassName: UserLoginSuccessService
* @Description: java类作用描述
* @Author: xiaLin
* @Date: 2023/5/16 22:52
* @Version: 1.0
*/
public interface UserLoginSuccessService {
/**
* 权限认证成功
*
* @param sessionId 会话id
* @param authentication 权限
* @throws IOException 异常信息
* @throws ServletException 异常信息
*/
void onAuthenticationSuccess(String sessionId, Authentication authentication) throws IOException, ServletException;
}
实现 UserLoginSuccessServiceImpl
ini
package com.cdkjframework.security.service.impl;
import com.cdkjframework.config.CustomConfig;
import com.cdkjframework.constant.BusinessConsts;
import com.cdkjframework.constant.CacheConsts;
import com.cdkjframework.constant.IntegerConsts;
import com.cdkjframework.entity.user.BmsConfigureEntity;
import com.cdkjframework.entity.user.ResourceEntity;
import com.cdkjframework.entity.user.RoleEntity;
import com.cdkjframework.entity.user.WorkflowEntity;
import com.cdkjframework.entity.user.security.SecurityUserEntity;
import com.cdkjframework.redis.RedisUtils;
import com.cdkjframework.security.service.*;
import com.cdkjframework.util.encrypts.JwtUtils;
import com.cdkjframework.util.encrypts.Md5Utils;
import com.cdkjframework.util.tool.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import jakarta.servlet.ServletException;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @ProjectName: cdkj-framework
* @Package: com.cdkjframework.security.service.impl
* @ClassName: UserLoginSuccessServiceImpl
* @Description: java类作用描述
* @Author: xiaLin
* @Date: 2023/5/16 22:52
* @Version: 1.0
*/
@Component
@RequiredArgsConstructor
public class UserLoginSuccessServiceImpl implements UserLoginSuccessService {
/**
* 自定义配置
*/
private final CustomConfig customConfig;
/**
* 配置服务
*/
private final ConfigureService configureServiceImpl;
/**
* 资源服务
*/
private final ResourceService resourceServiceImpl;
/**
* 用户角色
*/
private final UserRoleService userRoleServiceImpl;
/**
* 工作流服务
*/
private final WorkflowService workflowServiceImpl;
/**
* 有效时间
*/
private final long EFFECTIVE = IntegerConsts.TWENTY_FOUR * IntegerConsts.SIXTY * IntegerConsts.SIXTY;
/**
* 权限认证成功
*
* @param sessionId 会话id
* @param authentication 权限
* @throws IOException 异常信息
* @throws ServletException 异常信息
*/
@Override
public void onAuthenticationSuccess(String sessionId, Authentication authentication) throws IOException, ServletException {
SecurityUserEntity user = (SecurityUserEntity) authentication.getPrincipal();
// 构建 token
buildJwtToken(user, sessionId);
// 获取配置信息
BmsConfigureEntity configure = new BmsConfigureEntity();
configure.setOrganizationId(user.getOrganizationId());
configure.setTopOrganizationId(user.getTopOrganizationId());
List<BmsConfigureEntity> configureList = configureServiceImpl.listConfigure(configure);
user.setConfigureList(configureList);
// 用户角色
List<RoleEntity> roleList = userRoleServiceImpl.listRoleByUserId(user.getUserId());
if (!CollectionUtils.isEmpty(roleList)) {
// 用户资源
List<ResourceEntity> resourceList = resourceServiceImpl.listResource(roleList, user.getUserId());
// 用户资源写入缓存
String key = CacheConsts.USER_RESOURCE + user.getUserId();
RedisUtils.syncEntitySet(key, resourceList, EFFECTIVE);
user.setResourceList(resourceList);
}
// 查询工作流信息
WorkflowEntity workflow = new WorkflowEntity();
workflow.setOrganizationId(user.getOrganizationId());
workflow.setTopOrganizationId(user.getTopOrganizationId());
workflowServiceImpl.listWorkflow(workflow);
}
/**
* 生成 jwt token
*
* @param user 用户实体
* @param sessionId 会话id
*/
private void buildJwtToken(SecurityUserEntity user, String sessionId) {
// 生成 JWT token
Map<String, Object> map = new HashMap<>(IntegerConsts.FOUR);
map.put(BusinessConsts.LOGIN_NAME, user.getUsername());
long time = System.currentTimeMillis() / IntegerConsts.ONE_THOUSAND;
map.put(BusinessConsts.TIME, time);
map.put(BusinessConsts.USER_NAME, user.getUsername());
map.put(BusinessConsts.USER_TYPE, user.getUserType());
map.put(BusinessConsts.DISPLAY_NAME, user.getDisplayName());
map.put(BusinessConsts.TIME, time);
// 暂不需要该参数
String userAgent = StringUtils.Empty;
StringBuilder builder = new StringBuilder();
builder.append(String.format("loginName=%s&effective=%s&time=%s&userAgent=%s",
user.getUsername(), EFFECTIVE, time, userAgent));
String token = Md5Utils.getMd5(builder.toString());
map.put(BusinessConsts.HEADER_TOKEN, token);
String jwtToken = JwtUtils.createJwt(map, customConfig.getJwtKey());
user.setToken(jwtToken);
String tokenKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + sessionId;
RedisUtils.syncSet(tokenKey, jwtToken, IntegerConsts.SIXTY);
// 票据 token 关系
String ticketKey = CacheConsts.USER_PREFIX + BusinessConsts.HEADER_TOKEN + StringUtils.HORIZONTAL + token;
RedisUtils.syncSet(ticketKey, jwtToken, IntegerConsts.SIXTY);
// 用户信息写入缓存
String key = CacheConsts.USER_LOGIN + token;
RedisUtils.syncEntitySet(key, user, EFFECTIVE);
}
}
其中 ConfigureService、ResourceService、UserRoleService、WorkflowService服务接口需根据自己的业务进行相应的调整也可以直接使用项目,在实际服务中直接继承即可。
总结
以上只是博主自己在实际项目中总结出来,然后在将其实封成工具分享给大家。
相关源码在:维基框架
Gitee: gitee.com/cdkjframewo...
Github:github.com/cdkjframewo...
如果喜欢博主的分享记得给博主点点小星星