0%

Springboot集成Shiro


一、介绍

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。本文是使用Shiro + JWT(Json Web Token)实现的,对于jwt部分有疑问的可以参考之前jwt相关文章,本博文中的一部分函数使用的是JWT那一篇文章文章所写的函数。

参考博文 https://blog.csdn.net/w_stronger/article/details/73109248


二、创建项目

使用idea创建空项目并添加以下依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>


三、创建自定义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
66
67
68
69
70
71
72
//MyRealm.java
@Service
public class MyRealm extends AuthorizingRealm {

//用户相关
@Autowired
private UserDao userDao;

//shiro数据相关
@Autowired
private ShiroService shiroService;

/**
* 大坑!,必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}

/**
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户token
String token = (String) principals.getPrimaryPrincipal();

//通过token获取用户信息
String username = JWTUtil.getUsername(token);
User user = userDao.getUserAllInfoByTel(username);
int userId = user.getId();

//获取用户拥有操作
Set<String> permsSet = shiroService.listUserPerms(userId);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permsSet);

//获取用户拥有角色
Set<String> rolesSet = shiroService.listUserRoles(userId);
info.setRoles(rolesSet);

return info;
}

/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();

// 解密获得username,用于和数据库进行对比,此部分的身份验证请参考我的JWT相关博文
String username = JWTUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("Username or password error");
}

User user = userDao.getUserAllInfoByTel(username);
if (user == null) {
throw new AuthenticationException("Username or password error");
}

//验证信息
if (!JWTUtil.verify(token, username, user.getPassword())) {
throw new AuthenticationException("Username or password error");
}

return new SimpleAuthenticationInfo(token, token, "myRealm");
}

}


四、自定义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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//JWTFilter.java
public class JWTFilter extends BasicHttpAuthenticationFilter {

private Logger logger = LoggerFactory.getLogger(getClass());

/**
* 这里我们详细说明下为什么最终返回的都是true,即允许访问
* 例如我们提供一个地址 GET /article
* 登入用户和游客看到的内容是不同的
* 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
* 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 查看当前Header中是否携带Authorization属性(Token),有的话就进行登录认证授权
if (this.isLoginAttempt(request, response)) {
try {
// 进行Shiro的登录UserRealm
this.executeLogin(request, response);
} catch (Exception e) {
// 认证出现异常,传递错误信息msg
String msg = e.getMessage();
// 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
Throwable throwable = e.getCause();
if (throwable instanceof SignatureVerificationException) {
// 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
msg = "Token或者密钥不正确(" + throwable.getMessage() + ")";
} else if (throwable instanceof TokenExpiredException) {
} else {
// 应用异常不为空
if (throwable != null) {
// 获取应用异常msg
msg = throwable.getMessage();
}
}
/*
错误两种处理方式
1. 将非法请求转发到/401的Controller处理,抛出自定义无权访问异常被全局捕捉再返回Response信息
2. 无需转发,直接返回Response信息
一般使用第二种(更方便)
*/
return false;
}
} else {
// 没有携带Token
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
// 获取当前请求类型
String httpMethod = httpServletRequest.getMethod();
// 获取当前请求URI
String requestURI = httpServletRequest.getRequestURI();
logger.info("当前请求 {} Authorization属性(Token)为空 请求类型 {}", requestURI, httpMethod);
}
return true;
}

/**
* 判断用户是否想要登入。
* 检测header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("AuthKey");
return authorization != null;
}

/**
* 进行AccessToken登录认证授权
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("AuthKey");
JWTToken token = new JWTToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}

/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}

/**
* 将非法请求跳转到 /401
*/
private void response401(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/401");
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}


五、添加配置文件

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
//ShiroConfig.java
@Configuration
public class ShiroConfig {

@Bean("securityManager")
public DefaultWebSecurityManager getManager(MyRealm realm) {
//使用自定义realm
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(realm);

/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);

return manager;
}

@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);

// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);

/*
* 自定义url规则
* http://shiro.apache.org/web.html#urls-
*/
Map<String, String> filterRuleMap = new HashMap<>();
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/users/login", "anon");
filterRuleMap.put("/users", "jwt");
filterRuleMap.put("/users/*", "jwt");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);

return factoryBean;
}

/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}

@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}

注:一定要将所有需要判断拦截的请求使用自定义的拦截器进行拦截



6、使用注解对接口进行权限判断以及身份验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 这个函数的意思是需要请求这个接口的用户角色需要时admin,
* 同时具有sys:user:save的权限
*/
@PostMapping
@RequiresRoles("admin")
@RequiresPermissions(value = "sys:user:save")
public RestResult addUser(@RequestBody User user) {
logger.debug("/users [Post] -> user : {}", user);
try {
return userService.insertUser(user);
} catch (Exception e) {
return RestResultFactory.restResult(402, "用户创建失败");
}
}


7、对于常见注解的介绍

Shiro共有5个注解,接下来我们就详细说说吧


1.RequiresAuthentication:

  • 使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。

2.RequiresGuest:

  • 使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。

3.RequiresPermissions:

  • 当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。

4.RequiresRoles:

  • 当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当前Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。

5.RequiresUser

  • 当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

使用方法:

Shiro的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关):

  1. RequiresRoles
  2. RequiresPermissions
  3. RequiresAuthentication
  4. RequiresUser
  5. RequiresGuest
    例如:你同时声明了RequiresRoles和RequiresPermissions,那就要求拥有此角色的同时还得拥有相应的权限。

1) RequiresRoles

可以用在Controller或者方法上。可以多个roles,多个roles时默认逻辑为 AND也就是所有具备所有role才能访问。

1
2
3
4
5
6
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles {
String[] value();
Logical logical() default Logical.AND;
}


示例

1
2
3
4
5
6
7
8
//属于user角色
@RequiresRoles("user")

//必须同时属于user和admin角色
@RequiresRoles({"user","admin"})

//属于user或者admin之一;修改logical为OR 即可
@RequiresRoles(value={"user","admin"},logical=Logical.OR)


2) RequiresPermissions

1
2
3
4
5
6
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {
String[] value();
Logical logical() default Logical.AND;
}


示例

1
2
3
4
5
6
7
8
//符合index:hello权限要求
@RequiresPermissions("index:hello")

//必须同时复核index:hello和index:world权限要求
@RequiresPermissions({"index:hello","index:world"})

//符合index:hello或index:world权限要求即可
@RequiresPermissions(value={"index:hello","index:world"},logical=Logical.OR)

1) RequiresAuthentication,RequiresUser,RequiresGuest

这三个的使用方法一样

  • @RequiresAuthentication
  • @RequiresUser
  • @RequiresGusst

总结:总体来说,shiro的存在大大的降低了关于权限部分的开发时间,使我们有更多精力关注业务开发,也让我们能快速的开发出一个完善的权限系统,避免自己开发权限时出现的权限考虑不周到的情况