springboot-sample
介绍
springboot简单示例-使用JWT进行授权认证 跳转到发行版 查看发行版说明
软件架构(当前发行版)
- Springboot3.1.3
- hutool
- bcprov-jdk18on
安装教程
git clone --branch 自定义加密进行登录验证 git@gitee.com:simen_net/springboot-sample.git
主要功能
使用JWT认证
-
-
在WebSecurityConfig.java中注册JwtAuthenticationSuccessHandler.java和JwtAuthenticationFailureHandler.java验证处理器
// 注册验证成功处理器 httpSecurityFormLoginConfigurer.successHandler(authenticationSuccessHandler); // 注册验证失败处理器 httpSecurityFormLoginConfigurer.failureHandler(authenticationFailureHandler);
-
在WebSecurityConfig.java中加入异常处理器JwtAuthenticationEntryPoint
// 加入异常处理器 httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(authenticationEntryPoint) );
-
在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_write")); mapProperties.put("扩展属性", username + " file_write"); log.info("读取到已有用户[{}],默认密码123456,file_write权限,扩展属性:[{}]", username, mapProperties); return new JwtUserDetails(username, SM2_OBJ.signHex("123456", SecurityUtils.STR_UUID), listGrantedAuthority, mapProperties);
-
在SecurityUtils.java中定义全局常量
Map<String, String> MAP_LOGIN_SUCCESS
,用于保存用户登录时的token必,可以在服务器通过简单的匹配防止伪造签名,如不需要可以取消 -
登录成功处理器JwtAuthenticationSuccessHandler.java,返回包含jwt编码的标准化json。其中使用Sm2JwtSigner.java进行签名和校验
@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() ); // 在全局登录成功的map中放入当前用户登录的token MAP_LOGIN_SUCCESS.put(userDetails.getUsername(), strToken); // 包装返回的JWT对象 ReplyVO<JwtResponseData> replyVO = new ReplyVO<>( new JwtResponseData(strToken, DateUtil.date()), "用户登录成功"); // 将返回字符串写入response SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK, replyVO); } }
-
登录失败处理器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())); }
-
异常处理器JwtAuthenticationEntryPoint中根据request头Accept判断请求类型是html还是json,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)); } } }
-
登出成功处理器JwtLogoutSuccessHandler中
jwtTokenUtils.verifyToken(strJwtToken); // 从token中获取用户名 String strUserName = jwtTokenUtils.getAudience(strJwtToken); // 断言用户名非空 Assert.notBlank(strUserName, "当前用户不存在"); // 从全局登录信息Map中移除该用户信息 MAP_LOGIN_SUCCESS.remove(strUserName); log.info("[{}]登出成功", strUserName);
-
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) { StringBuilder sbContent = new StringBuilder(); sbContent.append(headerBase64).append(STR_JWT_SIGN_SPLIT).append(payloadBase64); return Base64Encoder.encode(SM2_OBJ.sign(StrUtil.utf8Bytes(sbContent))); } /** * 验签 * * @param headerBase64 JWT头的JSON字符串Base64表示 * @param payloadBase64 JWT载荷的JSON字符串Base64表示 * @param signBase64 被验证的签名Base64表示 * @return 签名是否一致 */ @Override public boolean verify(String headerBase64, String payloadBase64, String signBase64) { StringBuilder sbContent = new StringBuilder(); sbContent.append(headerBase64).append(STR_JWT_SIGN_SPLIT).append(payloadBase64); return SM2_OBJ.verify(StrUtil.utf8Bytes(sbContent), Base64Decoder.decode(signBase64)); }
-
生成的JWT代码和解密内容
-
-
JWT Tokens 编码
eyJ0eXAiOiJKV1QiLCJhbGciOiLlm73lr4ZTTTLpnZ7lr7nnp7Dnrpfms5XvvIzln7rkuo5CQ-W6kyJ9.eyJhdWQiOlsic2ltZW4iXSwiaWF0IjoxNjk1MDIwMzUzLCJleHAiOjE2OTUwMzgzNTMsIlVTRVJfQVVUSE9SSVRZIjoiZmlsZV9yZWFkIiwiTUFQX1VTRVJfUFJPUEVSVElFUyI6eyLmianlsZXlsZ7mgKciOiJzaW1lbiBmaWxlX3JlYWQifX0.MEQCIBr7QHoMdgqt53AM+hlVJfDfSrj8Pdi+dAJ9hg3QMBQuAiAhcFbV26ESehhylWewr467GNWncKruz86NfD68CU105Q==
-
Decode 解码后HEADER
{ "typ": "JWT", "alg": "国密SM2非对称算法,基于BC库" }
-
Decode 解码后PAYLOAD
{ "aud": [ "simen" ], "iat": 1695020353, "exp": 1695038353, "USER_AUTHORITY": "file_read", "MAP_USER_PROPERTIES": { "扩展属性": "simen file_read" } }
-
-