赞
踩
参考文章: https://blog.csdn.net/qq_35642036/article/details/82788588
官网(JWT): https://jwt.io/introduction
官方文章(Bearer认证定义细节 == 必须看 == 英语菜逼得自行谷歌翻译): https://datatracker.ietf.org/doc/html/rfc6750
注意: 登录成功的响应报文中有个基于用户信息生成一个token字段,下一次新请求时每次在请求头都会携带token值过去给服务端,服务端在校验token值是否合法,合法则说明用户有权限访问链接,不合法直接响应报文返回报错或者401
官方:Bearer认证失败响应案例(400、401、402)
官方:授权成功(登录成功)后获取token的响应案例
文件结构
application.yml
server: port: 8080 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true cache-enabled: true mapper-locations: classpath:mapper/*Mapper.xml global-config: db-config: id-type: assign_uuid logic-delete-value: 1 logic-not-delete-value: 0 logic-delete-field: is_del where-strategy: not_empty update-strategy: not_empty insertStrategy: not_empty spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver username: root password: root url: jdbc:p6spy:mysql://localhost:3306/lrc_blog?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai freemarker: suffix: .html
MyTokenUtil.java
package work.linruchang.qq.mybaitsplusjoin.common.util; import cn.hutool.core.codec.Base64; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Console; import cn.hutool.core.lang.Dict; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.HMac; import cn.hutool.crypto.digest.HmacAlgorithm; import cn.hutool.json.JSONUtil; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Date; import java.util.List; import java.util.Optional; /** * * 根据<a href="https://jwt.io/introduction"></a> 进行构建标准Token * @author LinRuChang * @version 1.0 * @date 2022/08/06 * @since 1.8 **/ public class MyTokenUtil { private static Dict HEADER = Dict.create(); static { //签名算法可以是HS256 或者 RSA 我这里写死成HS256算法 HEADER.set("alg", "HS256") .set("typ", "JWT"); } private static Dict DEFAULT_PAYLOAD = Dict.create(); static { DEFAULT_PAYLOAD.set("iss", "发行系统:mybaits-plus-join项目") .set("exp", null) //过期时间 毫秒 .set("cre", null) //创建时间 毫秒 .set("sub ", "可访问资源:系统任何资源都可访问") .set("aud", "授权给谁:超级管理员"); } /** * 签名密钥 */ private static final String secret = "sdfsdfds"; /** * 生成token * * @param payload 负载 * @return */ public static String createToken(Dict payload) { Dict allPayload = new Dict(DEFAULT_PAYLOAD); if (CollUtil.isNotEmpty(payload)) { allPayload.putAll(payload); } if(ObjectUtil.isEmpty(allPayload.get("cre"))) { //创建时间 allPayload.put("cre", new Date().getTime()); } String HEADERJson = JSONUtil.toJsonStr(HEADER); String HEADERJson_Base64Url = Base64.encodeUrlSafe(HEADERJson); String allPayloadJson = JSONUtil.toJsonStr(allPayload); String allPayload_Base64Url = Base64.encodeUrlSafe(allPayloadJson); HMac mac = new HMac(HmacAlgorithm.HmacSHA256, secret.getBytes()); String signSouceContent = StrUtil.format("{}.{}", HEADERJson_Base64Url, allPayload_Base64Url); String signature = mac.digestBase64(signSouceContent, true); return StrUtil.format("{}.{}.{}", HEADERJson_Base64Url, allPayload_Base64Url, signature); } /** * 校验token * 签名匹配且未过期 * @param token * @return */ public static boolean verify(String token) { List<String> splitContent = StrUtil.split(token, "."); boolean verifyFlag = false; if (CollUtil.size(splitContent) == 3) { String HEADERJson_Base64Url = splitContent.get(0); String allPayload_Base64Url = splitContent.get(1); String signature = splitContent.get(2); //系统根据前面头信息以及负载生成签名 HMac mac = new HMac(HmacAlgorithm.HmacSHA256, secret.getBytes()); String systemSignSourceContent = StrUtil.format("{}.{}", HEADERJson_Base64Url, allPayload_Base64Url); String systemSignature = mac.digestBase64(systemSignSourceContent, true); //比对签名 if (StrUtil.equals(systemSignature, signature)) { String allPayloadJson = Base64.decodeStr(allPayload_Base64Url); Dict dict = JSONUtil.toBean(allPayloadJson, Dict.class); String exp = dict.getStr("exp"); //比对token有效期 if (StrUtil.isBlank(exp) || (exp != null && new Date().getTime() < dict.getLong("exp"))) { verifyFlag = true; } } } return verifyFlag; } /** * 获取负载信息 * @param token * @return */ public static Dict parse(String token) { if (verify(token)) { String allPayloadJson = Base64.decodeStr(StrUtil.split(token, ".").get(1)); return JSONUtil.toBean(allPayloadJson, Dict.class); } return null; } /** * 根据Bearer认证标准从用户请求中获取token <a href=“https://datatracker.ietf.org/doc/html/rfc6750”></a> * @return */ public static String getCurrentRequestToken() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String authorization = request.getHeader("Authorization"); String token = null; //请求头 if(StrUtil.isNotBlank(authorization)) { List<String> authorizationInfos = StrUtil.splitTrim(authorization, StrUtil.SPACE); if(CollUtil.size(authorizationInfos) == 2 && StrUtil.equals(authorizationInfos.get(0), "Bearer")) { token = authorizationInfos.get(1); } } else if(StrUtil.equalsIgnoreCase(request.getMethod(),"GET") || (StrUtil.equalsIgnoreCase(request.getMethod(),"POST") && StrUtil.containsIgnoreCase(request.getHeader("Content-Type"),"application/x-www-form-urlencoded"))) { token = request.getParameter("access_token"); } return Optional.ofNullable(token) .orElse(null); } public static void main(String[] args) { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiLlj5HooYzns7vnu5_vvJpteWJhaXRzLXBsdXMtam9pbumhueebriIsInN1YiAiOiLlj6_orr_pl67otYTmupDvvJrns7vnu5_ku7vkvZXotYTmupDpg73lj6_orr_pl64iLCJhdWQiOiLmjojmnYPnu5nosIHvvJrotoXnuqfnrqHnkIblkZgiLCJ1c2VyTmFtZSI6ImxyYyJ9._Au456JLDQ4yIlkBYo8xiHTklrn1b2AMp46KHuKrfIU"; boolean verify = verify(token); Dict parseInfo = parse(token); Console.log(verify); Console.log(parseInfo); } }
BearerAuthInterceptor.java
/** * 作用:Bearer认证-其实就是token认证 * * @author LinRuChang * @version 1.0 * @date 2022/08/04 * @since 1.8 **/ @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class BearerAuthInterceptor implements HandlerInterceptor { /** * 认证错误信息 */ @AllArgsConstructor @Getter enum AuthErrorEnum { INVALID_REQUEST(400, "invalid_request", "请求不符合Bearer认证标准"), INVALID_TOKEN(401, "invalid_token", "认证失败"), INSUFFICIENT_SCOPE(402, "insufficient_scope", "token权限不足"); Integer errorCode; String error; String errorDescription; } /** * key:token * List<String>:token可访问的链接 */ public final static ConcurrentHashMap<String, List<String>> allSystemTokensMap = new ConcurrentHashMap(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { boolean authStatusFlag = false; AuthErrorEnum authErrorEnum = AuthErrorEnum.INVALID_TOKEN; String token = MyTokenUtil.getCurrentRequestToken(); //当前的请求头token String uri = request.getRequestURI(); //当前的uri链接 if (StrUtil.isNotBlank(token)) { boolean verifyToken = MyTokenUtil.verify(token); if (CollUtil.contains(allSystemTokensMap.keySet(), token) && verifyToken) { List<String> pathPatternList = allSystemTokensMap.get(token); if (CollUtil.isNotEmpty(pathPatternList)) { AntPathMatcher antPathMatcher = new AntPathMatcher(); authStatusFlag = pathPatternList.stream().anyMatch(pathPattern -> antPathMatcher.match(pathPattern, uri)); if (!authStatusFlag) { //此token权限不足以访问此请求内容 authErrorEnum = AuthErrorEnum.INSUFFICIENT_SCOPE; } } else { authStatusFlag = true; } } else { //token非法或过期,从系统allSystemTokens中排除 allSystemTokensMap.remove(token); } } else { //token缺失-没传或者没按规范进行装填Token authErrorEnum = AuthErrorEnum.INVALID_REQUEST; } //认证失败 if (!authStatusFlag) { response.setStatus(authErrorEnum.getErrorCode()); response.setHeader("WWW-Authenticate", StrUtil.format("Bearer realm=\"admin token\";charset=UTF-8;error=\"{}\";error_description=\"{}\"", authErrorEnum.getError(), URLUtil.encode(authErrorEnum.getErrorDescription()))); response.setHeader("Content-Type", "application/json;charset=UTF-8"); response.getWriter().write(JSONUtil.toJsonStr(Dict.create() .set("msg", authErrorEnum.getErrorDescription()) .set("code", authErrorEnum.getErrorCode()))); } return authStatusFlag; } }
MyConfig.java
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Autowired
BearerAuthInterceptor bearerAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(bearerAuthInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/js/**","/user*/**","/user*/**");
}
}
ArticleCategoryController.java
@RestController @RequestMapping("article-category") public class ArticleCategoryController { @Autowired ArticleCategoryService articleCategoryService; /** * 根据ID进行查询 * @param id * @return */ @GetMapping("/one/{id}") public CommonHttpResult<ArticleCategory> findById(@PathVariable("id") String id) { return CommonHttpResult.success(articleCategoryService.getById(id)); } }
ArticleCommentController.java
@RestController @RequestMapping("article-comment") public class ArticleCommentController { @Autowired ArticleCommentService articleCommentService; /** * 根据ID进行查询 * @param id * @return */ @GetMapping("/one/{id}") public CommonHttpResult<ArticleComment> findById(@PathVariable("id") String id) { return CommonHttpResult.success(articleCommentService.getById(id)); } }
UserController3.java
@Controller @RequestMapping("user3") public class UserController3 { /** * 登录页面 * * @param httpServletRequest * @param modelAndView * @return */ @GetMapping("login") public ModelAndView loginPage(HttpServletRequest httpServletRequest, ModelAndView modelAndView) { modelAndView.setViewName("login3"); return modelAndView; } /** * 登录 * * @param httpServletRequest * @param httpServletResponse * @param modelAndView */ @PostMapping("login") @SneakyThrows @ResponseBody public Dict login(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, ModelAndView modelAndView) { String userName = httpServletRequest.getParameter("userName"); String password = httpServletRequest.getParameter("password"); if (StrUtil.equals(userName, "admin") && StrUtil.equals(password, "admin123")) { Dict tokenPayload = Dict.create() .set("num", UUID.fastUUID().toString(true)) .set("userName", userName) .set("credentials", SecureUtil.md5(StrUtil.format("{}:{}", userName, password))); String accessToken = MyTokenUtil.createToken(tokenPayload); BearerAuthInterceptor.allSystemTokensMap.put(accessToken, Arrays.asList("/article-category/**")); return Dict.create(). set("code","success"). set("msg","登录成功==每次请求其他接口时请将access_token放置到请求头上传给服务器"). set("access_token",accessToken). set("expires_in", -1). set("token_type","Bearer"); } return Dict.create(). set("code","failure"). set("msg","登录失败"); } /** * 退出登录 == 命令浏览器删除cookie * * @return */ @GetMapping("logout") @ResponseBody public Dict logout() { String currentRequestToken = MyTokenUtil.getCurrentRequestToken(); BearerAuthInterceptor.allSystemTokensMap.remove(currentRequestToken); return Dict.create() .set("code", "success") .set("msg", StrUtil.isNotBlank(currentRequestToken) ? StrUtil.format("token已失效:{}", currentRequestToken) : null); } }
login3.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="/js/jquery.min.js"></script> <style> .testRequest { display: block; margin-bottom: 15px; } </style> </head> <body> <table border="1"> <form action="/user3/login" method="post"> <tr> <td>账号:</td> <td><input id="userName" name="userName" type="text"></td> </tr> <tr> <td>密码:</td> <td><input id="password" name="password" type="text"></td> </tr> <tr> <td colspan="2" style="text-align: center"> <!--<button id="loginBtn" type="submit">登录</button>--> <button id="loginBtn" type="button">登录</button> <!--<button id="logoutBtn" type="button" ><a href="/user3/logout?access_token=">退出</a></button>--> <button id="logoutBtn" type="button" >退出</button> </td> </tr> </form> </table> <div><span style="font-weight: bolder">当前用户Token:</span><span id="currentUser"></span></div> <div style="margin-top: 20px;"> 请填入access_token:<input id="accessToken" type="text" value="" placeholder="请填入access_token"> </div> <a target="_blank" id="testRequest1" tar class="testRequest" href="/article-category/one/c08d391e02bc11eb9416b42e99ea3e62?access_token=">测试请求(token有权限):<span id="testRequest1Href">/article-category/one/c08d391e02bc11eb9416b42e99ea3e62?access_token2=</span></a> <a target="_blank" id="testRequest2" class="testRequest" href="/article-comment/one/1346e1060e95de36d8d8a7bbc8925dfb?access_token=">测试请求(token无访问权限):<span id="testRequest2Href">/article-category/one/c08d391e02bc11eb9416b42e99ea3e62?access_token2=</span></a> </body> <script> $(function () { var logoutLink = '/user3/logout?access_token='; $("input[id='accessToken']").bind('input propertychange', function() { var access_token = $(this).val(); logoutLink = logoutLink.substring(0,logoutLink.indexOf('=')) + '=' + access_token; $(".testRequest").each(function (index) { href = $(this).attr("href") console.log(href) var hrefPrefix = href.substring(0,href.indexOf('=')) var newHref = hrefPrefix + '=' + access_token; $(this).attr("href", newHref); $('#testRequest' + (index+1) + 'Href').text(newHref) }) }); $("#currentUser").text(localStorage.getItem("access_token")) /** * 登录 */ $("#loginBtn").click(function () { var userName = $("#userName").val(); var password = $("#password").val(); $.post("/user3/login",{ userName: userName, password : password }, function (data) { console.log(data) if(data.access_token) { localStorage.setItem("access_token",data.access_token); $("#currentUser").text(localStorage.getItem("access_token")) } alert(data.msg + ": " + (data.access_token?data.access_token:'')); },'json') }) /** * 退出 */ $("#logoutBtn").click(function () { var inputToken = $("input[id='accessToken']").val() if(!$("input[id='accessToken']").val()) { alert("请输入需要退出的Token") return; } if(localStorage.getItem("access_token") == inputToken) { localStorage.removeItem("access_token") $("#currentUser").text('') } console.log('退出:' + logoutLink) $.get(logoutLink,{}, function (data) { alert(data.msg) },'json') }) }) </script> </html>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。