Springboot简单功能示例-5 使用JWT进行授权认证
springboot-sample
介绍
软件架构(当前发行版使用)
安装教程
git clone --branch 5.使用JWT进行授权认证 git@gitee.com:simen_net/springboot-sample.git
功能说明
在WebSecurityConfig中配置自定义的JWT认证
/** * 用户验证服务 {@link JwtUserDetailsService} */ private final UserDetailsService userDetailsService; /** * 身份验证成功处理程序 {@link JwtAuthenticationSuccessHandler} */ private final AuthenticationSuccessHandler authenticationSuccessHandler; /** * 身份验证失败的处理程序 {@link JwtAuthenticationFailureHandler} */ private final AuthenticationFailureHandler authenticationFailureHandler; /** * 登出成功处理程序 {@link JwtLogoutSuccessHandler} */ private final LogoutSuccessHandler logoutSuccessHandler; /** * JWT认证入口点 {@link JwtAuthenticationEntryPoint} */ private final AuthenticationEntryPoint authenticationEntryPoint; /** * JWT请求过滤 */ private final JwtRequestFilter jwtRequestFilter;
发行版说明
- 完成基本WEB服务 跳转到发行版
- 完成了KEY初始化功能和全局错误处理 跳转到发行版
- 完成了基本登录验证 跳转到发行版
- 完成了自定义加密进行登录验证 跳转到发行版
- 完成了自定义加密进行登录验证 跳转到发行版 查看发行版说明
使用JWT进行授权认证
配置Config
-
在WebSecurityConfig.java中加入“注册验证成功/失败处理器”JwtAuthenticationSuccessHandler.java和JwtAuthenticationFailureHandler.java
// 注册验证成功处理器 httpSecurityFormLoginConfigurer.successHandler(authenticationSuccessHandler); // 注册验证失败处理器 httpSecurityFormLoginConfigurer.failureHandler(authenticationFailureHandler); -
在WebSecurityConfig.java中加入“JWT认证入口点”JwtAuthenticationEntryPoint,请求无认证信息时在此处理
// 加入异常处理器 httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> // 加入JWT认证入口点 httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(authenticationEntryPoint) ); -
在WebSecurityConfig.java中加入“登出成功处理器”JwtLogoutSuccessHandler.java注销用户登录信息等
// 自定义登出成功处理器 httpSecurityLogoutConfigurer.logoutSuccessHandler(logoutSuccessHandler); -
在WebSecurityConfig.java登出过滤器之前加入“JWT请求过滤器”JwtRequestFilter.java对所有请求进行鉴权
// 在登出过滤器之前加入JWT请求过滤器 httpSecurity.addFilterBefore(jwtRequestFilter, LogoutFilter.class); -
在WebSecurityConfig.java中强制session无效
// 强制session无效,使用jwt认证时建议禁用,正常登录不能禁用session httpSecurity.sessionManagement(httpSecuritySessionManagementConfigurer-> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) );
全局说明
-
在JwtUserDetails.java中增加
private Map<String, Object> mapProperties
,用于保存登录用户的扩展信息,录入用户分组、用户单位等等 -
在JwtUserDetailsService.java中模拟注入用户权限及扩展信息
listGrantedAuthority.add(new SimpleGrantedAuthority("file_read")); mapProperties.put("扩展属性", username + " file_read"); log.info("读取到已有用户[{}],默认密码123456,file_read权限,扩展属性:[{}]", username, mapProperties); return new JwtUserDetails(username, SecurityUtils.signByUUID("123456"), false, listGrantedAuthority, mapProperties);` -
在SecurityUtils.java中定义全局登录信息MAP,保存用户的token和验证对象。一是防止用户伪造token,二是缓存用户验证对象
/** * 【系统】用户名与JWT Token对应的map * key: 用户登录名 * value: JWT Token */ public static Map<String, String> MAP_SYSTEM_USER_TOKEN = new ConcurrentHashMap<>(8); /** * 【系统】用户名与 UsernamePasswordAuthenticationToken 对应的map * key: 用户登录名 * value: UsernamePasswordAuthenticationToken */ public static Map<String, UsernamePasswordAuthenticationToken> MAP_SYSTEM_USER_AUTHENTICATION = new ConcurrentHashMap<>(8); -
在SystemErrorController中重写
BasicErrorController
的public ResponseEntity<Map<String, Object>> error(HttpServletRequest request)
,将包括JWT处理在内的各类服务异常进行统一处理@Override public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { HttpStatus status = this.getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity<>(status); } else { Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL)); log.info("非HTML请求返回错误:{}", body); // 获取http返回状态码 Integer intStatus = MapUtil.getInt(body, "status"); // 获取http返回的异常字符串 String strException = MapUtil.getStr(body, "exception"); // 返回对象的消息 String strMsg = LOGIN_ERROR; // 返回对象的内容 String strData = null; // 直接从request中获取STR_JAKARTA_SERVLET_ERROR_EXCEPTION对象 Object objErrorException = request.getAttribute(STR_JAKARTA_SERVLET_ERROR_EXCEPTION); // 1. 使用request的STR_JAKARTA_SERVLET_ERROR_EXCEPTION值获取错误消息 // 判断异常对象是否为空 if (ObjUtil.isNotNull(objErrorException)) { List<String> lisErrorException = StrUtil.splitTrim(objErrorException.toString(), ":"); if (lisErrorException.size() == 2) { String strTemp = MAP_EXCEPTION_MESSAGE.get(lisErrorException.get(0)); if (StrUtil.isNotBlank(strTemp)) { strMsg = strTemp; strData = lisErrorException.get(1); } } } // 2. 使用request的exception字符串获取错误消息 // 判断replyVO.getData()为空,且http返回的异常字符串是否为空 if (StrUtil.isBlank(strData) && StrUtil.isNotBlank(strException)) { strData = MAP_EXCEPTION_MESSAGE.get(strException); } // 3. 使用request的exception字符串获取错误消息 // 判断replyVO.getData()为空,且错误代码有效 if (StrUtil.isBlank(strData) && intStatus > 0) { ReplyEnum replyEnum = EnumUtil.getBy(ReplyEnum.class, re -> re.getCode().equals(intStatus)); // 判断错误代码获取到的枚举类是否存在 if (ObjUtil.isNotNull(replyEnum)) { strData = replyEnum.getMsg(); } } // 4. 使用默认错误消息 // 判断replyVO.getData()为空 if (StrUtil.isBlank(strData)) { // 默认返回的错误内容 strData = LOGIN_ERROR_UNKNOWN; } return new ResponseEntity<>(JSON.toMap(new ReplyVO<>(strData, strMsg, intStatus)), HttpStatus.OK); } } -
测试流程:访问 http://localhost:8080/login
登录流程
-
无权限访问时,转到JWT认证入口点JwtAuthenticationEntryPoint,根据request头Accept判断请求类型是html还是json,html请求跳转到登录页面,json请求返回异常接送代码【该功能主要为演示,使用JWT时实际很少出现需要同时处理html和json请求的情况】
@Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 从request头中获取Accept String strAccept = request.getHeader("Accept"); if (StrUtil.isNotBlank(strAccept)) { // 对Accept分组为字符串数组 String[] strsAccept = StrUtil.splitToArray(strAccept, ","); // 判断Accept数组中是否存在"text/html" if (ArrayUtil.contains(strsAccept, "text/html")) { // 存在"text/html",判断为html访问,则跳转到登录界面 response.sendRedirect(STR_URL_LOGIN_URL); } else { // 不存在"text/html",判断为json访问,则返回未授权的json SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK, new ReplyVO<>(ReplyEnum.ERROR_TOKEN_EXPIRED)); } } } -
登录成功时,调用处理器JwtAuthenticationSuccessHandler.java,其中使用Sm2JwtSigner.java进行签名和校验。更新该用户的
MAP_SYSTEM_USER_TOKEN
,删除该用户的MAP_SYSTEM_USER_AUTHENTICATION
@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { if (!response.isCommitted() && authentication != null && authentication.getPrincipal() != null // 获取登录用户信息对象 && authentication.getPrincipal() instanceof JwtUserDetails userDetails) { // 获取30分钟有效的token编码 String strToken = jwtTokenUtils.getToken30Minute( userDetails.getUsername(), CollUtil.join(userDetails.getAuthorities(), ","), userDetails.getMapProperties() ); // 更新系统缓存的用户JWT Token MAP_SYSTEM_USER_TOKEN.put(userDetails.getUsername(), strToken); // 删除系统缓存的用户身份验证对象 MAP_SYSTEM_USER_AUTHENTICATION.remove(userDetails.getUsername()); // 包装返回的JWT对象 ReplyVO<JwtResponseData> replyVO = new ReplyVO<>( new JwtResponseData(strToken, DateUtil.date()), "用户登录成功"); // 将返回字符串写入response SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK, replyVO); log.info("[{}]登录成功,已缓存该用户Token", userDetails.getUsername()); } } -
登录失败时,调用处理器JwtAuthenticationFailureHandler,根据抛出的异常返回对应的json
@Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { String strData = LOGIN_ERROR_UNKNOWN; String strMessage = "LOGIN_ERROR_UNKNOWN"; if (exception instanceof LockedException) { strData = LOGIN_ERROR_ACCOUNT_LOCKING; strMessage = exception.getMessage(); } else if (exception instanceof CredentialsExpiredException) { strData = LOGIN_ERROR_PASSWORD_EXPIRED; strMessage = exception.getMessage(); } else if (exception instanceof AccountExpiredException) { strData = LOGIN_ERROR_OVERDUE_ACCOUNT; strMessage = exception.getMessage(); } else if (exception instanceof DisabledException) { strData = LOGIN_ERROR_ACCOUNT_BANNED; strMessage = exception.getMessage(); } else if (exception instanceof BadCredentialsException) { strData = LOGIN_ERROR_USER_CREDENTIAL_EXCEPTION; strMessage = exception.getMessage(); } else if (exception instanceof UsernameNotFoundException) { strData = LOGIN_ERROR_USER_NAME_NOT_FOUND; strMessage = exception.getMessage(); } // exception.printStackTrace(); SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK, new ReplyVO<>(strData, strMessage, ReplyEnum.ERROR_USER_HAS_NO_PERMISSIONS.getCode())); } -
正常请求json时,使用过滤器JwtRequestFilter.java,对每个JSON请求进行鉴权(其中使用
MAP_SYSTEM_USER_AUTHENTICATION
进行缓存处理),并将相应信息放入SpringSecurity的上下文身份验证中SecurityContextHolder.getContext().setAuthentication(authenticationToken);
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 如果不是访问登出url,且通过认证 if (!StrUtil.equals(URLUtil.getPath(request.getRequestURL().toString()), STR_URL_LOGOUT_URL) && SecurityContextHolder.getContext().getAuthentication() == null) { // 获取请求头Authorization final String strAuthorization = request.getHeader(HttpHeaders.AUTHORIZATION); // 判断请求Authorization非空且以STR_AUTHENTICATION_PREFIX开头 if (StrUtil.isNotBlank(strAuthorization) && strAuthorization.startsWith(STR_AUTHENTICATION_PREFIX)) { // 获取JWT Token String strJwtToken = strAuthorization.replace(STR_AUTHENTICATION_PREFIX, ""); // 验证凭证,失败则抛出错误 jwtTokenUtils.verifyToken(strJwtToken); // 从JWT Token中获取用户名 String strUserName = jwtTokenUtils.getAudience(strJwtToken); // 从系统MAP中获取该用户的身份验证对象 UsernamePasswordAuthenticationToken authentication = MAP_SYSTEM_USER_AUTHENTICATION.get(strUserName); // 判断身份验证对象非空 if (ObjUtil.isNotEmpty(authentication)) { // 放入安全上下文中 SecurityContextHolder.getContext().setAuthentication(authentication); log.info(String.format("检测到[%s]访问,从系统MAP中直接获取身份验证对象", strUserName)); } else { // 从JWT Token中获取权限字符串 String strAuthorities = jwtTokenUtils.getAuthorities(strJwtToken); // 将用户权限放入权限列表 List<GrantedAuthority> listGrantedAuthority = new ArrayList<>(); if (StrUtil.isNotBlank(strAuthorities)) { String[] strsAuthority = StrUtil.splitToArray(strAuthorities, ","); for (String strAuthority : strsAuthority) { listGrantedAuthority.add(new SimpleGrantedAuthority(strAuthority.trim())); } } // 构建用户登录信息实现 JwtUserDetails userDetails = new JwtUserDetails( strUserName, // 获取用户名 "[PROTECTED]", // 屏蔽密码 jwtTokenUtils.isToRefresh(strJwtToken), // 从token获取jwt认证是否需要刷新 listGrantedAuthority, jwtTokenUtils.getUserPropertiesMap(strJwtToken)); // 构建用户认证token UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, listGrantedAuthority); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 放入安全上下文中 SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 将身份验证对象放入系统MAP MAP_SYSTEM_USER_AUTHENTICATION.put(strUserName, authenticationToken); log.info(String.format("检测到[%s]访问,具有[%s]权限,缓存至系统MAP", userDetails.getUsername(), strAuthorities)); } } } // 使用过滤链进行过滤 filterChain.doFilter(request, response); } -
登出成功时,调用处理器JwtLogoutSuccessHandler,并清空该用户的
MAP_SYSTEM_USER_TOKEN
和MAP_SYSTEM_USER_AUTHENTICATION
缓存@Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { // 从Request中取出授权字符串 final String strAuthorization = request.getHeader(HttpHeaders.AUTHORIZATION); // 判断授权字符串是否以STR_AUTHENTICATION_PREFIX开头 if (StrUtil.startWith(strAuthorization, STR_AUTHENTICATION_PREFIX)) { // 获取认证的JWT token String strJwtToken = strAuthorization.replace(STR_AUTHENTICATION_PREFIX, ""); // 判断token是否为空 if (StrUtil.isNotBlank(strJwtToken)) { // 验证凭证,失败则抛出错误 try { jwtTokenUtils.verifyToken(strJwtToken); // 从token中获取用户名 String strUserName = jwtTokenUtils.getAudience(strJwtToken); // 断言用户名非空 Assert.notBlank(strUserName, "当前用户不存在"); // 删除系统缓存的用户JWT Token MAP_SYSTEM_USER_TOKEN.remove(strUserName); // 删除系统缓存的用户身份验证对象 MAP_SYSTEM_USER_AUTHENTICATION.remove(strUserName); log.info("[{}]登出成功,已清除该用户登录缓存信息", strUserName); } catch (Exception ignored) { log.info("登出失败"); } } } // 返回登出成功信息 SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK, new ReplyVO<>(LOGOUT_SUCCESS)); }
JWT处理
-
Sm2JwtSigner.java签名和校验时,将
headerBase64
和payloadBase64
使用STR_JWT_SIGN_SPLIT
组合成字符串进行签名和校验/** * 返回签名的Base64代码 * * @param headerBase64 JWT头的JSON字符串的Base64表示 * @param payloadBase64 JWT载荷的JSON字符串Base64表示 * @return 签名结果Base64,即JWT的第三部分 */ @Override public String sign(String headerBase64, String payloadBase64) { // 将headerBase64和payloadBase64使用STR_JWT_SIGN_SPLIT组合在一起之后进行签名 return SecurityUtils.signByUUID(headerBase64 + STR_JWT_SIGN_SPLIT + payloadBase64); } /** * 验签 * * @param headerBase64 JWT头的JSON字符串Base64表示 * @param payloadBase64 JWT载荷的JSON字符串Base64表示 * @param signBase64 被验证的签名Base64表示 * @return 签名是否一致 */ @Override public boolean verify(String headerBase64, String payloadBase64, String signBase64) { // 将headerBase64和payloadBase64使用STR_JWT_SIGN_SPLIT组合在一起之后进行签名校验 return SecurityUtils.verifyByUUID(headerBase64 + STR_JWT_SIGN_SPLIT + payloadBase64, signBase64); } -
生成的JWT代码和解密内容
-
JWT Tokens 编码
eyJ0eXAiOiJKV1QiLCJhbGciOiLlm73lr4ZTTTLpnZ7lr7nnp7Dnrpfms5XvvIzln7rkuo5CQ-W6kyJ9.eyJhdWQiOlsic2ltZW4iXSwiaWF0IjoxNjk1MDIwMzUzLCJleHAiOjE2OTUwMzgzNTMsIlVTRVJfQVVUSE9SSVRZIjoiZmlsZV9yZWFkIiwiTUFQX1VTRVJfUFJPUEVSVElFUyI6eyLmianlsZXlsZ7mgKciOiJzaW1lbiBmaWxlX3JlYWQifX0.MEQCIBr7QHoMdgqt53AM+hlVJfDfSrj8Pdi+dAJ9hg3QMBQuAiAhcFbV26ESehhylWewr467GNWncKruz86NfD68CU105Q== -
解码后HEADER
{ "typ": "JWT", "alg": "国密SM2非对称算法,基于BC库" } -
解码后PAYLOAD
{ "aud": [ "simen" ], "iat": 1695020353, "exp": 1695038353, "USER_AUTHORITY": "file_read", "MAP_USER_PROPERTIES": { "扩展属性": "simen file_read" } }
-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异