Bearer认证(JWT-Token)- 用户信息存客户端中
讲解(Bearer认证)
参考文章: 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的响应案例
实现(Bearer认证)
代码(Bearer认证)
文件结构
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>