springboot2.0 + shiro集成(一)初识shiro

生平所幸皆历历,负尽苍生不负卿。

Shiro是一个Java平台的开源权限框架,用于认证和访问授权。

shiro架构简介

在shiro架构中,有3个最主要的组件:Subject,SecurityManager,Realm。

  1. Subject本质上就是当前访问用户的抽象描述。
  2. SecurityManager是Shiro架构中最核心的组件,通过它可以协调其他组件完成用户认证和授权。相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
  3. Realm定义了访问数据的方式,用来连接不同的数据源,如:数据库,配置文件等等。可以有1个或多个Realm(多个Realm时可以是共同验证,也可以不同realm对应不同业务),可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm。

从应用程序角度看shiro的工作流程:
从应用程序角度看shiro的工作流程

shiro内部架构:
shiro内部架构
由上图看出,除了三个主要组件之外,shiro还包含了以下几个组件:

  1. Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  2. Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  3. SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);
  4. SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
  5. CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能;
  6. Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

本篇主要介绍一下shiro如何与springboot整合。下面通过一个登录验证业务介绍一下。

使用步骤:

版本信息:
springboot:2.1.6
shiro:1.4.0

1、pom配置

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

2、编写自定义realm

自定义realm需要重写AuthorizingRealm 的doGetAuthenticationInfo和doGetAuthorizationInfo方法,前者在调用subject.login()方法是会被调用,用于做认证,后者在使用@Require…等鉴权配置的时候会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 用户名密码登录
@PostMapping("/dologin")
@ResponseBody
public BackAdminResult dologin(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("rememberMe") boolean rememberMe,
HttpSession session) throws AuthenticationException {
//rememberMe 记住我
UsernamePasswordToken token = new UsernamePasswordToken(username, password,rememberMe);
Subject subject = SecurityUtils.getSubject();
try {
//subject.getSession().setTimeout(180000000);
subject.login(token);
} catch (IncorrectCredentialsException ice) {
return BackAdminResult.build(1, "密码错误");
}
User user = (User) subject.getPrincipal();
return BackAdminResult.build(0, "登录成功!");
}

3、自定义业务过滤器

shiro中已经提供了很多内置的Filter,其中常见的有anon,authc,perms,roles,也可以覆写方法实现自定义filter,以满足自己的业务需求。
isAccessAllowed方法和onAccessDenied方法,只要两者有一个可以就可以了,从名字中我们也可以理解,逻辑是这样:先调用isAccessAllowed,如果返回的是true,则直接放行执行后面的filter和servlet,如果返回的是false,则继续执行后面的onAccessDenied方法,如果后面返回的是true则也可以有权限继续执行后面的filter和servelt。只有两个函数都返回false才会阻止后面的filter和servlet的执行。

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
public class OAuth2Filter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
/* if (StringUtils.isBlank(token)) {
return null;
}*/
//return new OAuth2Token(token);
return null;
}

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request);
/*if (StringUtils.isBlank(token)) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
String json = new Gson().toJson(JsonResult.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));
httpResponse.getWriter().print(json);
return false;
}*/
return executeLogin(request, response);
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
/*// httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
JsonResult r = JsonResult.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());
String json = new Gson().toJson(r);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
e1.printStackTrace();
}*/
return false;
}
/**
* 获取请求的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
/*if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter("token");
}
*/
return token;
}
}

4、ShiroConfig配置

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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package com.skn.keelin.shiro.config;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.Filter;

import org.apache.shiro.authc.AbstractAuthenticator;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import com.skn.keelin.shiro.config.redis.RedisSessionDAO;
import com.skn.keelin.shiro.config.redis.ShiroRedisCache;
import com.skn.keelin.shiro.config.redis.ShiroRedisCacheManager;
import com.skn.keelin.shiro.oauth2.realms.PhoneRealm;
import com.skn.keelin.shiro.oauth2.realms.UserRealm;

@Configuration
public class ShiroConfig {

@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 加密策略
*/
@Bean
public CredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 加密算法:MD5、SHA1
credentialsMatcher.setHashAlgorithmName("md5");
// 散列次数:md5(md5("")) 默认一次
credentialsMatcher.setHashIterations(1);
return credentialsMatcher;
}

/**
* 自定义Realm
*/
@Bean
public UserRealm userRealm(CredentialsMatcher credentialsMatcher) {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(credentialsMatcher);
// userRealm.setCacheManager(shiroCacheManager());
return userRealm;
}


@Bean
public SecurityManager securityManager(UserRealm userRealm, PhoneRealm phoneRealm,
AbstractAuthenticator abstractAuthenticator) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realms
List<Realm> realms = new ArrayList<Realm>();
realms.add(userRealm);
securityManager.setRealms(realms);

return securityManager;
}


@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SessionManager sessionManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//oauth过滤
Map<String, Filter> filters = new HashMap<String, Filter>();
shiroFilter.setFilters(filters);
filters.put("oauth2", new OAuth2Filter());
filters.put("rememberMe", new RememberAuthenticationFilter());
//filters.put("logout", new SystemLogoutFilter());

//过滤链定义,从上向下顺序执行 一般将/**放在最为下边
Map<String, String> filterMap = new LinkedHashMap<String, String>();
filterMap.put("/sys/login", "anon");
filterMap.put("/captcha", "anon");
// 注册用户
filterMap.put("/sys/user/reg", "anon");
//swagger
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/v2/api-docs-ext", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/doc.html", "anon");

filterMap.put("/druid/**", "anon");
// api接口放权
filterMap.put("/api/**", "anon");
// websocket 放权
filterMap.put("/ws/**", "anon");
// 静态资源
filterMap.put("/static/**", "anon");
filterMap.put("/user/dologin", "anon");

filterMap.put("/user/plogin", "anon");

filterMap.put("/hello", "authc");//测试多线程用
filterMap.put("/getTicket", "anon");//测试消息队列用
filterMap.put("/getTicket1", "anon");//测试消息队列用
filterMap.put("/getTicket2", "anon");//测试消息队列用
filterMap.put("/redis", "anon");//测试redis集群用
filterMap.put("/services/**", "anon");//测试webservice接口用

// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilter.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilter.setSuccessUrl("/index");
// 未授权界面;
shiroFilter.setUnauthorizedUrl("/403"); //自己写403页面

//filterMap.put("/**", "oauth2");
//filterMap.put("/**", "rememberMe");
filterMap.put("/logout", "logout");


shiroFilter.setFilterChainDefinitionMap(filterMap);

return shiroFilter;
}


/**
*
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}

}

5、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 用户名密码登录
@PostMapping("/dologin")
@ResponseBody
public BackAdminResult dologin(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("rememberMe") boolean rememberMe,
HttpSession session) throws AuthenticationException {

//rememberMe 记住我
UsernamePasswordToken token = new UsernamePasswordToken(username, password,rememberMe);

Subject subject = SecurityUtils.getSubject();
try {
//subject.getSession().setTimeout(180000000);
subject.login(token);
} catch (IncorrectCredentialsException ice) {
return BackAdminResult.build(1, "密码错误");
}

User user = (User) subject.getPrincipal();
return BackAdminResult.build(0, "登录成功!");

}

登录成功


-------------本文结束感谢您的阅读-------------