Spring Boot 项目整合Spring Security 进行身份验证
前言
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。与所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以很容易地进行扩展以满足自定义要求。
引入库
<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
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
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
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
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
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等接口
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
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
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
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
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
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
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
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
该抽象类需要应用服务端实现
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
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
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
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
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: https://gitee.com/cdkjframework/wiki-framework
Github:https://github.com/cdkjframework/wiki-framework
如果喜欢博主的分享记得给博主点点小星星

浙公网安备 33010602011771号