springboot整合shiro与jwt
jwt 介绍
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
硬翻译:JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公用/专用密钥对对JWT进行签名。
Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.
硬翻译:尽管可以对JWT进行加密以提供双方之间的保密性,但我们将重点关注已签名的令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌则将这些声明隐藏在其他方的面前。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是对其进行签名的一方。
—— jwt 官网介绍
jwt 能做的事
Authorization: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Single Sign On is a feature that widely uses JWT nowadays, because of its small overhead and its ability to be easily used across different domains.
授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
Information Exchange: JSON Web Tokens are a good way of securely transmitting information between parties. Because JWTs can be signed—for example, using public/private key pairs—you can be sure the senders are who they say they are. Additionally, as the signature is calculated using the header and the payload, you can also verify that the content hasn’t been tampered with.
信息交换:JSON Web令牌是在各方之间安全传输信息的一种好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
jwt 组成结构
- Header
- Payload
- Signature
整体看起来像:
1 | xxxxx.yyyyy.zzzzz |
header 通常由两部分组成:令牌的类型为JWT,以及所使用的签名算法,例如HMAC SHA256或RSA。
例如:
1 | { |
此JSON被Base64Url编码以形成JWT的第一部分。
payload 令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。共有三种类型的索赔:注册,公共和私人索赔。
例如:
1 | { |
此JSON被Base64Url编码以形成JWT的第二部分。
Signature 要创建签名部分,您必须获取编码的标头,编码的有效载荷,机密,标头中指定的算法,并对其进行签名。
例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名
1 | HMACSHA256( |
java 中使用
官网 提供了多种 Java 的实现。具体不同实现之间的区别请参考这篇博客:https://andaily.com/blog/?p=956
这里以 jose4j 为例 (官方代码:https://bitbucket.org/b_c/jose4j/wiki/JWT%20Examples)
1 | // |
运行下 控制台输出为:
1 | JWT: eyJraWQiOiJrMSIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiJJc3N1ZXIiLCJhdWQiOiJBdWRpZW5jZSIsImV4cCI6MTYwOTAzMzExOSwianRpIjoiczlLa2hzMEZpOTU1QjNWaW0tZkV1dyIsImlhdCI6MTYwOTAzMjUxOSwibmJmIjoxNjA5MDMyMzk5LCJzdWIiOiJzdWJqZWN0IiwiZW1haWwiOiJtYWlsQGV4YW1wbGUuY29tIiwiZ3JvdXBzIjpbImdyb3VwLW9uZSIsIm90aGVyLWdyb3VwIiwiZ3JvdXAtdGhyZWUiXX0.ag7oSV0pW6z0N8YmDEfEoXyZWgycSLHsbMjP46dH0cbpamI5cW7jdYHAom-LPviIvTE8k5raWJktgYu0s5vxMzJON95vzQvsxzHVS0kSrKOUZVONRWn4M7mZa56gV9wVYFlcisF-hejNDS-vV7gJN5HEGaU8iiLyYFtSXEftSIj_kbKALCswWc1cRKfJOVnLhMy4xEM3VXevcsRWpOVtt_-h_C4wEzdRPvnuqMKUZwjVwiKV8VDbTnuQCPn9fQ8OThjSZ4lMhTNI2bgls0uglEeEOL5iFGW_orzSv0trW2FbENlwAi41RjIpi3_ga3wPQUPzi6-Pf6YrM5rVVZ3xKg |
springboot 集成 shiro
吐槽 :去 shiro 官网查看文档,没有看到 springboot 相关的内容,最接近的也只是 Java web app 的文档。然后百度了大量的博客。发现了 shiro 的启动器。然后去 maven 仓库查 shiro 的启动器。但是不知道怎么用。
难道别人家的 springboot 的例子配置都是自己看源码得出的么。然后继续百度。终于有人说到了。
传统web方式(前后端不分离)
shiro pom 依赖
1
2
3
4
5<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.0</version>
</dependency>application.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14#启用/禁用 shiro starter 默认 true
shiro.web.enabled=true
#这里如果配置了 url 。
# shiro 的认证 filter 会自动将 post /login 的请求视作登录 并从请求中获取参数 来执行 shiro 的 login 逻辑。
# 且 shiro 会将 get /login 视作获取登录页面。当未认证的请求被拦截后 shiro 会重定向到 get /login.
# 注意:如果没配置 loginUrl 那么 shiro 会默认把 get /login.jsp 路径当作登录页(即认证失败会往这里跳转让你去登录)
# 相关源代码 参看 FormAuthenticationFilter.onAccessDenied() 方法。
#shiro.loginUrl=/login
#如果配置了此参数。shiro 的认证 filter 在处理成功登录后会跳转到这里的url。
#注意:shiro 会记住上次登录的地址。如果有上次访问的地址 那么会重定向上次的地址。
# 相关源代码 参看 FormAuthenticationFilter.onLoginSuccess()
#shiro.successUrl=/successShiroConfig.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74package com.wzy.platform.config;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @ClassName ShiroConfig
* @Author wuzhiyong
* @Date 2020/12/27 12:26
* @Version 1.0
**/
public class ShiroConfig {
/**
* 配置密码加密 规则
* @return
*/
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//md5加密1次
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(1);
return hashedCredentialsMatcher;
}
public Realm realm() {
ShiroCustomRealm customRealm = new ShiroCustomRealm();
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customRealm;
}
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
return securityManager;
}
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/static/**", "anon");
chainDefinition.addPathDefinition("/login", "anon");
// 其他的权限这里我们都通过注解的方式来控制
// logged in users with the 'admin' role
// chainDefinition.addPathDefinition("/admin/**", "authc, roles[admin]");
// // logged in users with the 'document:read' permission
// chainDefinition.addPathDefinition("/docs/**", "authc, perms[document:read]");
// chainDefinition.addPathDefinition("/logout", "authc");
// chainDefinition.addPathDefinition("/**", "anon");
return chainDefinition;
}
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
creator.setUsePrefix(true);
return creator;
}
}ShiroCustomRealm.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59package com.wzy.platform.config;
import com.wzy.platform.model.SysUser;
import com.wzy.platform.service.SysPermissionService;
import com.wzy.platform.service.SysRoleService;
import com.wzy.platform.service.SysUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* @ClassName ShiroCustomRealm
* @Author wuzhiyong
* @Date 2020/12/28 8:40
* @Version 1.0
**/
public class ShiroCustomRealm extends AuthorizingRealm {
private static final Logger LOGGER = LoggerFactory.getLogger(ShiroCustomRealm.class);
private SysUserService sysUserService;
private SysPermissionService sysPermissionService;
private SysRoleService sysRoleService;
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUser sysUser = (SysUser) principals.getPrimaryPrincipal();
//查询 权限
List<String> sysPermissions = sysPermissionService.selectPermissionByUserId(sysUser.getUserId());
//查询 角色
List<String> sysRoles = sysRoleService.selectRolesByUserId(sysUser.getUserId());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(sysPermissions);
info.addRoles(sysRoles);
LOGGER.info("doGetAuthorizationInfo");
return info;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//通过 用户名 查询用户对象
SysUser sysUser = sysUserService.findByUserName(token.getUsername());
if (sysUser == null) {
return null;
}
LOGGER.info("doGetAuthenticationInfo");
return new SimpleAuthenticationInfo(sysUser, sysUser.getPassword().toCharArray(), ByteSource.Util.bytes(sysUser.getSalt()), getName());
}
}LoginController.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87package com.wzy.platform.web.mvc;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @ClassName LoginController
* @Author wuzhiyong
* @Date 2020/12/28 22:26
* @Version 1.0
**/
public class LoginController {
public String index(){
return "index";
}
public String success(){
return "success";
}
/**
* get请求,登录页面跳转
* @return
*/
public String login(Model model) {
//如果已经认证通过,直接跳转到首页
if (SecurityUtils.getSubject().isAuthenticated()) {
return "redirect:/index";
}
model.addAttribute("message","wuzhiyong");
return "login";
}
/**
* post表单提交,登录
* @param username
* @param password
* @param model
* @return
*/
public Object login(String username, String password, Model model) {
Subject user = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
//shiro帮我们匹配密码什么的,我们只需要把东西传给它,它会根据我们在UserRealm里认证方法设置的来验证
user.login(token);
return "redirect:/success";
} catch (UnknownAccountException e) {
//账号不存在和下面密码错误一般都合并为一个账号或密码错误,这样可以增加暴力破解难度
model.addAttribute("message", "账号不存在!");
} catch (DisabledAccountException e) {
model.addAttribute("message", "账号未启用!");
} catch (IncorrectCredentialsException e) {
model.addAttribute("message", "密码错误!");
} catch (Throwable e) {
model.addAttribute("message", "未知错误!");
}
return "success";
}
/**
* 退出
* @return
*/
public String logout() {
SecurityUtils.getSubject().logout();
return "redirect:/login";
}
}
三个简单页面:
index.html
1
2
3
4
5
6
7
8
9
10
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
index page
</body>
</html>login.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${message}">${message}</h1>
<form method="post" action="/login">
用户名:<input name="username"><br>
密码:<input name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>success.html
1
2
3
4
5
6
7
8
9
10
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test success</title>
</head>
<body>
test success!
</body>
</html>
权限注解
1 | package com.wzy.platform.web.rest; |
用户与密码
id | user_name | full_name | password | salt |
---|---|---|---|---|
1 | zhangsan | 张三 | 86fb1b048301461bdc71d021d2af3f97 | 1 |
2 | lisi | 李四 | c9351e5cf153923f052ebe1462cca93a | 2 |
3 | wangwu | 王五 | 92262648696eae1b0a321cbd2b238bf2 | 3 |
4 | user1 | 用户1 | 86fb1b048301461bdc71d021d2af3f97 | 4 |
密码可通过如下方法加密:
1 | import org.apache.shiro.crypto.hash.SimpleHash; |
优化
1 | package org.apache.shiro.web.util; |
番外篇(简化代码)
如果我们的登录方式只是 用户名/密码 的方式登录的话。我们可以简化一部分代码。细心的同学是可以从上面的 application.properties 代码中看出一些端倪。
首先配置:
1 | shiro.loginUrl=/login |
ShiroConfig 中去除这段代码:
1 | chainDefinition.addPathDefinition("/login", "anon"); |
LoginController 中注释掉这段代码:
1 | /** |
好了,我们要登录的时候,直接请求 post /login 传上 username/password 就可以了。
原因就是利用 shiro 自带的认证逻辑 :源码 FormAuthenticationFilter.onAccessDenied
1 | protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { |
注意 由于 shiro 中在 request 中获取 用户名/密码 是固定的参数 uaername/password 。所以我们登录携带参数的 key 必须为:username 和 password 在源码中就可以看到:
1 | public class FormAuthenticationFilter extends AuthenticatingFilter { |
springboot 集成 shiro + jwt (前后端分离)
我们一边根据请求的过程一边来贴配置代码。
登录部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* post表单提交,登录
* @param username
* @param password
* @param model
* @return
*/
public Object login(HttpServletRequest request, String username, String password, Model model) throws JoseException {
// SysUser user = sysUserService.findByUserNameAndPwd(username,simpleHash.toString());
SysUser user = sysUserService.findByUserName(username);
if (user == null){
return ApiResult.fail("用户名错误!");
}
SimpleHash simpleHash = new SimpleHash("md5",password.toCharArray(), ByteSource.Util.bytes(user.getSalt()),1);
System.out.println(simpleHash);
System.out.println(user.getPassword());
if (simpleHash.toString().equals(user.getPassword())){
//签发 jwt 并返回
return ApiResult.ok(JwtUtils.getInstance().create(user));
}
return ApiResult.fail("用户名或密码错误!");
}登录成功返回一个 jwt 串。前端拿到后请求其它接口带上 jwt 即可。
过滤器部分
当请求路径被拦截后,来到这个过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import com.wzy.platform.common.ApiResult;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.PassThruAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Value;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/**
* @ClassName ShiroCustomFilter
* @Author wuzhiyong
* @Date 2021/1/15 13:33
* @Version 1.0
**/
public class ShiroCustomJwtFilter extends PassThruAuthenticationFilter {
String tokenName;
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
// if (isLoginRequest(request, response)) {
// if (isLoginSubmission(request, response)) {
//
// }
// return true;
// }
String jwt = getJwt(request);
if (StringUtils.isBlank(jwt)){
//todo 返回错误码
ApiResult.writeToJson(response, ApiResult.fail("请登录!"));
return false;
}
ShiroCustomJwtToken token = new ShiroCustomJwtToken(jwt);
Subject subject = getSubject(request, response);
try{
subject.login(token);
}catch (AuthenticationException e){
ApiResult.writeToJson(response, ApiResult.fail("身份认证失败!请重新登录。"));
}
return true;
}
String getJwt(ServletRequest request) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//从header中获取token
String token = httpServletRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = httpServletRequest.getParameter("token");
}
return token;
}
protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
}
}逻辑上 先判断请求中有没有 jwt 没有就返回 提示:”身份认证失败!请重新登录。”。如果有,那么就 把 jwt 使用 ShiroCustomJwtToken 包装后执行 shiro 的认证逻辑 subject.login(token);
自定义 Realm 部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import com.wzy.platform.common.utils.JwtUtils;
import com.wzy.platform.service.SysPermissionService;
import com.wzy.platform.service.SysRoleService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.lang.JoseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @ClassName ShiroCustomJwtRealm
* @Author wuzhiyong
* @Date 2021/1/25 9:32
* @Version 1.0
**/
public class ShiroCustomJwtRealm extends AuthorizingRealm {
private static final Logger LOGGER = LoggerFactory.getLogger(ShiroCustomRealm.class);
private SysPermissionService sysPermissionService;
private SysRoleService sysRoleService;
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
java.lang.String jwt = (java.lang.String) principals.getPrimaryPrincipal();
Long id = null;
try {
JwtUtils.getInstance().validateJwt(jwt);
id = Long.valueOf(JwtUtils.getInstance().getSubject(jwt));
} catch (MalformedClaimException | JoseException e) {
e.printStackTrace();
// 有可能 认证的时候 jwt 有效, 这里授权的时候 jwt 过期失效了。
throw new AuthorizationException("method AuthorizationInfo。get permissions or roles Exception in jwt");
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(sysPermissionService.selectPermissionByUserId(id));
info.addRoles(sysRoleService.selectRolesByUserId(id));
LOGGER.info("doGetAuthorizationInfo");
return info;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
ShiroCustomJwtToken token = (ShiroCustomJwtToken) authenticationToken;
LOGGER.info("doGetAuthenticationInfo");
return new SimpleAuthenticationInfo(new SimplePrincipalCollection(token.getPrincipal(),token.getPrincipal().toString()),token.getCredentials());
}
public boolean supports(AuthenticationToken token){
return token instanceof ShiroCustomJwtToken;
}
}Realm 的逻辑都一样 doGetAuthorizationInfo 这里拿出 jwt 后解析出 用户 id 然后再从数据库里查出角色和权限放入 SimpleAuthorizationInfo 中。doGetAuthenticationInfo 则是取出 jwt 并放入 SimpleAuthenticationInfo
自定义 token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.apache.shiro.authc.AuthenticationToken;
/**
* @ClassName ShiroCustomJwtToken
* @Author wuzhiyong
* @Date 2021/1/25 9:33
* @Version 1.0
**/
public class ShiroCustomJwtToken implements AuthenticationToken {
String jwt;
public ShiroCustomJwtToken(String jwt) {
this.jwt = jwt;
}
public Object getPrincipal() {
return jwt;
}
public Object getCredentials() {
return jwt;
}
}自定义 jwt 密码验证器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.wzy.platform.common.utils.JwtUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.jose4j.lang.JoseException;
/**
* @ClassName ShiroCustomJwtCredentialsMatcher
* @Author wuzhiyong
* @Date 2021/1/25 10:04
* @Version 1.0
**/
public class ShiroCustomJwtCredentialsMatcher implements CredentialsMatcher {
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
try {
if (JwtUtils.getInstance().validateJwt((String) token.getPrincipal())==null){
// throw new AuthenticationException("jwt 验证失败!");
return false;
}
} catch (JoseException e) {
e.printStackTrace();
return false;
}
return true;
}
}这里只需要通过 jwtUtils 来验证 jwt 就可以了
配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @ClassName ShiroConfig
* @Author wuzhiyong
* @Date 2020/12/27 12:26
* @Version 1.0
**/
public class ShiroConfig {
public Realm realm() {
//jwt realm
ShiroCustomJwtRealm customRealm = new ShiroCustomJwtRealm();
//配置 自定义的 jwt 密码验证器
customRealm.setCredentialsMatcher(new ShiroCustomJwtCredentialsMatcher());
customRealm.setCachingEnabled(false);
return customRealm;
}
/**
* 注入SessionStorageEvaluator,关闭Session存储
*/
public SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
//关闭会话存贮
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
return defaultSessionStorageEvaluator;
}
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
securityManager.setSubjectDAO(defaultSubjectDAO);
return securityManager;
}
public ShiroFilterFactoryBean shiroFilterFactoryBean () {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
// shiroFilterFactoryBean.setLoginUrl("/login");
// shiroFilterFactoryBean.setSuccessUrl("/");
// shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
//配置拦截器,实现无权限返回401,而不是跳转到登录页
filters.put("authc_jwt", new ShiroCustomJwtFilter());
//注意此处使用的是LinkedHashMap,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证
//所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/captcha.jpg", "anon");
filterChainDefinitionMap.put("/favicon.ico", "anon");
// filterChainDefinitionMap.put("/**", "authc_jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
creator.setUsePrefix(true);
return creator;
}
}配置完毕! 使用的时候。接口上加上相关注解就可以了。
异常类
UnknownAccountException (用户名错误或者不存在)
IncorrectCredentialsException(密码不正确)
AuthenticationException 异常是Shiro在登录认证过程中,认证失败需要抛出的异常。
AuthenticationException包含以下子类:
CredentitalsException 凭证异常
IncorrentCredentialsException 不正确的凭证
ExpiredCredentialsException 凭证过期
AccountException 账号异常
ConcurrentAccessException 并发访问异常(多个用户同时登录时抛出)
UnknownAccountException 未知的账号
ExcessiveAttemptsException 认证次数超过限制
DisabledAccountException 禁用的账号
LockedAccountException 账号被锁定
UnsupportedTokenException 使用了不支持的Token
注意
- shiro 的权限验证机制的逻辑与我们自己预想的可能会有不同。例如:
假设用户具有如下权限集合:
id | parent_id | res_name | res_type | permission |
---|---|---|---|---|
1 | 系统管理 | menu | m@sys | |
2 | 1 | 用户管理 | menu | m@sys:usr |
3 | 1 | 角色管理 | menu | m@sys:role |
4 | 一级菜单 | menu | m@lv1 | |
5 | 4 | 二级菜单1 | menu | m@lv1:xxx |
6 | 4 | 二级菜单2 | menu | m@lv1:yyy |
7 | 2 | 用户添加 | button | b@usradd |
方法上注解配置的权限为:
1 |
当这个用户登录后访问该方法是有权限的 即使 权限中 没有 “m@sys:role:edit”。
shiro 这里的规则是:[root]:[sub_1]:[sub_2]…… (参看Subject.isPermitted()源码)
即以冒号”:“作为分割。左边表示根节点右边表示子节点。如果该用户配置了根节点权限那么其就具备了所有其子节点的权限
上面中用户具有”m@sys“权限。那么”m@sys:usr“ 与 ”m@sys:role“、“m@sys:role:edit”权限就自动具备了。
(个人认为这样设计的目的是为了方便我们在树型目录中给用户来配置权限,只要配置了根节点就具备了其子节点的属性。)
那么反过来就说明权限数据只要这些就行了:
id | parent_id | res_name | res_type | permission |
---|---|---|---|---|
1 | 系统管理 | menu | m@sys | |
4 | 一级菜单 | menu | m@lv1 | |
7 | 2 | 用户添加 | button | b@usradd |
- shiro 可能会导致一些事务失效,详情请百度
- jwt 生成后如果服务重启 jwt 将失效。因为服务启动每执行 RsaJwkGenerator.generateJwk() 方法,实例不一样。
参考:
jwt 官网:https://jwt.io/
博客:https://andaily.com/blog/?p=956
博客:https://blog.51cto.com/wyait/2125708
apache shiro 官网 10 分钟教程:https://shiro.apache.org/10-minute-tutorial.html
apache shiro springboot 教程:https://shiro.apache.org/spring-boot.html
博客:https://segmentfault.com/a/1190000014479154
博客:https://segmentfault.com/a/1190000013630601?utm_source=sf-related