Shiro

Shiro能做什么?

  • Authentication 认证

  • Authorization 授权

  • Session Management 会话管理

  • Cryptgraphy 加密

img

Shiro核心组件

  1. Subject

    当前用户(当前访问系统的第三方进程)

  2. securityManager

    内部组件管理

  3. realm

    封装了一套安全相关的DAO层的 “领域”,一组认证和授权

SpringBoot 集成

  1. Maven 依赖
1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>

  1. 自定义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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
/**
* Shiro配置
* <p>
* 大部分参数都是在这里直接修改
*
* @author niczsama
* @date 2019/8/13
*/
@Configuration
public class ShiroConfig {

/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
* Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
*
* @return org.apache.shiro.spring.web.ShiroFilterFactoryBean
* @author niczsama
* @date 2019/8/15
*/
@Bean(name = "shirFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) {

//定义返回对象
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

//必须设置 SecurityManager,Shiro的核心安全接口
shiroFilterFactoryBean.setSecurityManager(securityManager);
//这里的/login是后台的接口名,非页面,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/sessionInvalid");
//这里的/index是后台的接口名,非页面,登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/");
//未授权界面,该配置无效,并不会进行页面跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/sessionInvalid");

//限制同一帐号同时在线的个数
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);

// 配置访问权限 必须是LinkedHashMap,因为它必须保证有序
// 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 一定要注意顺序,否则就不好使了
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//logout是shiro提供的过滤器
filterChainDefinitionMap.put("/logout", "logout");
//配置不登录可以访问的资源,anon 表示资源都可以匿名访问
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/u/**", "anon");
filterChainDefinitionMap.put("/swagger/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/simpleBillRepertory/getShareStockUrl", "anon");
filterChainDefinitionMap.put("/alicheck", "anon");
filterChainDefinitionMap.put("/api-docs/**", "anon");
//此时访问/userInfo/del需要del权限,在自定义Realm中为用户授权。
//filterChainDefinitionMap.put("/userInfo/del", "perms[\"userInfo:del\"]");

//其他资源都需要认证 authc 表示需要认证才能进行访问
filterChainDefinitionMap.put("/**", "kickout,authc");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

return shiroFilterFactoryBean;
}

/**
* 身份认证realm
*
* @return com.ccb.web.shiro.ShiroRealm
* @author niczsama
* @date 2019/8/15
*/
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCachingEnabled(true);
//启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
shiroRealm.setAuthenticationCachingEnabled(true);
//缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
shiroRealm.setAuthenticationCacheName("authenticationCache");
//启用授权缓存,即缓存AuthorizationInfo信息,默认false
shiroRealm.setAuthorizationCachingEnabled(true);
//缓存AuthorizationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
shiroRealm.setAuthorizationCacheName("authorizationCache");
return shiroRealm;
}

/**
* cookie管理对象;记住我功能,rememberMe管理器
*
* @return org.apache.shiro.web.mgt.CookieRememberMeManager
* @author niczsama
* @date 2019/8/15
*/
@Bean
public CookieRememberMeManager cookieRememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(simpleCookie());
//rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}

/**
* shiro缓存管理器
*
* @return org.apache.shiro.cache.ehcache.EhCacheManager
* @author niczsama
* @date 2019/8/15
*/
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return cacheManager;
}

/**
* redisTemplate 配置
*
* @param redisConnectionFactory
* @return
*/
@Bean("csRedisTemplate")
public RedisTemplate<String, Serializable> csRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
ObjectMapper om = new ObjectMapper();

// 4.设置可见度
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 5.启动默认的类型
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//解决实体缺少 set get 方法
om.configure(MapperFeature.USE_GETTERS_AS_SETTERS, false);
//截取实体缺少的属性
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer(om));
template.setHashKeySerializer(new StringRedisSerializer());
template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer(om));
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean("csRedisUtil")
public CsRedisUtil csRedisUtil(@Qualifier("csRedisTemplate") RedisTemplate<String, Serializable> redisTemplate) {
CsRedisUtil redisUtil = new CsRedisUtil();
redisUtil.setRedisTemplate(redisTemplate);
return redisUtil;
}


/**
* 配置会话管理器,设定会话超时及保存
*
* @return org.apache.shiro.session.mgt.SessionManager
* @author niczsama
* @date 2019/8/15
*/
@Bean("shiroSessionManager")
public ShiroSessionManager shiroSessionManager() {

ShiroSessionManager sessionManager = new ShiroSessionManager();
Collection<SessionListener> listeners = new ArrayList<>();
//配置监听
listeners.add(shiroSessionListener());
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionIdCookie(sessionIdCookie());
sessionManager.setSessionDAO(redisSessionDAO());
sessionManager.setCacheManager(ehCacheManager());

//全局会话超时时间(单位毫秒)
sessionManager.setGlobalSessionTimeout(60 * 1000 * 60);
//是否开启删除无效的session对象 默认为true
sessionManager.setDeleteInvalidSessions(true);
//是否开启定时调度器进行检测过期session 默认为true
sessionManager.setSessionValidationSchedulerEnabled(true);
//设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话
//设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
sessionManager.setSessionValidationInterval(60 * 1000 * 10);
sessionManager.setSessionFactory(shiroSessionFactory());

//取消url 后面的 JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);

return sessionManager;

}

/**
* 配置核心安全事务管理器
*
* @return org.apache.shiro.mgt.SecurityManager
* @author niczsama
* @date 2019/8/15
*/
@Bean(name = "securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置自定义realm.
securityManager.setRealm(shiroRealm());
//配置记住我
securityManager.setRememberMeManager(cookieRememberMeManager());
//配置缓存管理器
securityManager.setCacheManager(ehCacheManager());
//配置session管理器
securityManager.setSessionManager(shiroSessionManager());
return securityManager;
}

/**
* FormAuthenticationFilter 过滤器 过滤记住我
*
* @return org.apache.shiro.web.filter.authc.FormAuthenticationFilter
* @author niczsama
* @date 2019/8/15
*/
@Bean
public FormAuthenticationFilter formAuthenticationFilter() {
FormAuthenticationFilter formAuthenticationFilter = new FormAuthenticationFilter();
//对应前端的checkbox的name = rememberMe
formAuthenticationFilter.setRememberMeParam("rememberMe");
return formAuthenticationFilter;
}

/**
* 并发登录控制
*
* @return com.ccb.web.shiro.KickoutSessionControlFilter
* @author niczsama
* @date 2019/8/15
*/
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter() {
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
kickoutSessionControlFilter.setCacheManager(ehCacheManager());
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/login?kickout=1");
return kickoutSessionControlFilter;
}

/**
* 配置Shiro生命周期处理器
*
* @return org.apache.shiro.spring.LifecycleBeanPostProcessor
* @author niczsama
* @date 2019/8/15
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}



/**
* shiro缓存管理器
*
* @return com.ccb.web.shiro.RedisCacheManager
* @author niczsama
* @date 2019/8/15
*/
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
//redis中针对不同用户缓存
redisCacheManager.setPrincipalIdFieldName("username");
//用户权限信息缓存时间
redisCacheManager.setExpire(60 * 1000 * 60);
return redisCacheManager;
}

/**
* redis 替代默认缓存
*
* @return com.ccb.web.shiro.RedisSessionDAO
* @author niczsama
* @date 2019/8/15
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setExpire(60 * 60);
return redisSessionDAO;
}

/**
* SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
*
* @return org.apache.shiro.session.mgt.eis.SessionDAO
* @author niczsama
* @date 2019/8/15
*/
@Bean
public SessionDAO sessionDAO() {
RedisSessionDAO redisSessionDAO = redisSessionDAO();
//使用ehCacheManager
redisSessionDAO.setCacheManager(ehCacheManager());
//设置session缓存的名字 默认为 shiro-activeSessionCache
redisSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
//sessionId生成器
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
return redisSessionDAO;
}

/**
* 配置保存sessionId的cookie
* 注意:这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理 也需要自己的cookie
*
* @return org.apache.shiro.web.servlet.SimpleCookie
* @author niczsama
* @date 2019/8/15
*/
@Bean("sessionIdCookie")
public SimpleCookie sessionIdCookie() {
//这个参数是cookie的名称
SimpleCookie simpleCookie = new SimpleCookie("sid");
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
//maxAge=-1表示浏览器关闭时失效此Cookie
simpleCookie.setMaxAge(-1);
return simpleCookie;
}

/**
* 配置会话ID生成器
*
* @return org.apache.shiro.session.mgt.eis.SessionIdGenerator
* @author niczsama
* @date 2019/8/15
*/
@Bean
public SessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}



/**
* session工厂
*
* @return com.ccb.web.shiro.ShiroSessionFactory
* @author niczsama
* @date 2019/8/15
*/
@Bean("sessionFactory")
public ShiroSessionFactory shiroSessionFactory() {
ShiroSessionFactory sessionFactory = new ShiroSessionFactory();
return sessionFactory;
}

/**
* 配置session监听
*
* @return com.ccb.web.shiro.ShiroSessionListener
* @author niczsama
* @date 2019/8/15
*/
@Bean("sessionListener")
public ShiroSessionListener shiroSessionListener() {
ShiroSessionListener sessionListener = new ShiroSessionListener();
return sessionListener;
}

/**
* cookie对象;会话Cookie模板 ,默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid或rememberMe
*
* @return org.apache.shiro.web.servlet.SimpleCookie
* @author niczsama
* @date 2019/8/15
*/
@Bean
public SimpleCookie simpleCookie() {
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
/*
* 设为true后,只能通过http访问,javascript无法访问
* 防止xss读取cookie
*/
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
//<!-- 记住我cookie生效时间30天 ,单位秒;-->
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}

/**
* 解决: 无权限页面不跳转
*
* @return org.springframework.web.servlet.handler.SimpleMappingExceptionResolver
* @author niczsama
* @date 2019/8/15
* @apiNote shiroFilterFactoryBean.setUnauthorizedUrl(" / unauthorized ") 无效
* shiro的源代码ShiroFilterFactoryBean.Java定义的filter必须满足filter instanceof AuthorizationFilter,
* 只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,
* 所以unauthorizedUrl设置后页面不跳转 Shiro注解模式下,登录失败与没有权限都是通过抛出异常。
* 并且默认并没有去处理或者捕获这些异常。在SpringMVC下需要配置捕获相应异常来通知用户信息
*/
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
Properties properties = new Properties();
//这里的 /unauthorized 是页面,不是访问的路径
properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "/unauthorized");
properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "/unauthorized");
simpleMappingExceptionResolver.setExceptionMappings(properties);
return simpleMappingExceptionResolver;
}



/**
* 让某个实例的某个方法的返回值注入为Bean的实例
* Spring静态注入
*
* @return
*/
@Bean
public MethodInvokingFactoryBean getMethodInvokingFactoryBean() {
MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean();
factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
factoryBean.setArguments(new Object[]{securityManager()});
return factoryBean;
}

}
  1. 自定义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
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//获取 AuthorizationInfo 中的 principal
Object principal = subject.getPrincipal();
info.addStringPermission("user:add");
System.out.println("执行了授权");
return info;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证");
//数据库获取用户信息
String username = "123";
String password = "123";
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
if (!token.getUsername().equals(username)) {
return null;
}
//返回AuthenticationInfo (principal 主要信息-》用户本身,credentials 密码 ,credentialsSalt 盐值,realmName 当前realm的名字 )
return new SimpleAuthenticationInfo("",password,null,"");

}
}
  1. 自定义sessionManager
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
public class ShiroSessionManager extends DefaultWebSessionManager {

private static final String AUTHORIZATION = "authToken";

private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

public ShiroSessionManager(){
super();
}

@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response){
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
System.out.println("id:"+id);
if(StringUtils.isEmpty(id)){
//如果没有携带id参数则按照父类的方式在cookie进行获取
System.out.println("super:"+super.getSessionId(request, response));
return super.getSessionId(request, response);
}else{
//如果请求头中有 authToken 则其值为sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE);
return id;
}
}
}

需要在ShiroConfig中将自定义的SessionManager注入管理器中

1
2
3
4
5
6
7
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
  1. 自定义全局异常处理
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
public class GlobalExceptionResolver implements HandlerExceptionResolver {

private static Logger logger = LoggerFactory.getLogger(GlobalExceptionResolver.class);

@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView mv;
//进行异常判断。如果捕获异常请求跳转。
if(ex instanceof UnauthorizedException){
mv = new ModelAndView("/user/unauth");
return mv;
}else {
mv = new ModelAndView();
FastJsonJsonView view = new FastJsonJsonView();
BaseResponse baseResponse = new BaseResponse();
baseResponse.setMsg("服务器异常");
ex.printStackTrace();
logger.error(ExceptionUtils.getFullStackTrace(ex));
Map<String,Object> map = new HashMap<>();
String beanString = JSON.toJSONString(baseResponse);
map = JSON.parseObject(beanString,Map.class);
view.setAttributesMap(map);
mv.setView(view);
return mv;

}

}
}

将自定义全局异常处理注入Shiro中

1
2
3
4
5
6
7
8
/**
* 注册全局异常处理
* @return
*/
@Bean(name = "exceptionHandler")
public HandlerExceptionResolver handlerExceptionResolver(){
return new GlobalExceptionResolver();
}