Java Shiro 基础

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

三个核心组件:Subject, SecurityManager 和 Realms
Subject:即“当前操作用户”。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

自定义Shiro注解

  • 创建自定义的注解
  • 权限处理器,继承AuthorizingAnnotationHandler,校验权限
  • 资源管理器,继承AuthorizationAttributeSourceAdvisor,添加新注解支持
  • 方法拦截器,继承AuthorizingAnnotationMethodInterceptor
  • AOP拦截器,继承AopAllianceAnnotationsAuthorizingMethodInterceptor
  • shiroConfig配置
  • shiro UserRealm
  • 使用demo

创建注解

1
2
3
4
5
6
7
8
9
10
11
12
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
/**
* 权限路径:分隔
*
* @return
*/
String[] value();

Logical logical() default Logical.AND;
}

权限处理器

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 AuthHandler extends AuthorizingAnnotationHandler {
public AuthHandler() {
//写入注解
super(Auth.class);
}

@Override
public void assertAuthorized(Annotation a) throws AuthorizationException {
if (a instanceof Auth) {
Auth annotation = (Auth) a;
String[] perms = annotation.value();
//1.获取当前主题
Subject subject = this.getSubject();
//2.验证是否包含当前接口的权限有一个通过则通过
boolean hasAtLeastOnePermission = false;
for (String permission : perms) {
if (subject.isPermitted(permission)) {
hasAtLeastOnePermission = true;
break;
}
}
if (!hasAtLeastOnePermission) {
throw new AuthorizationException("没有访问此接口的权限");
}

}
}
}

资源管理器

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
@Component//加上该注解可以不用再shiroConfig文件中增加相关代码
public class ShiroAdvisor extends AuthorizationAttributeSourceAdvisor {

public ShiroAdvisor() {
// 这里可以添加多个
setAdvice(new AuthAopInterceptor());
}

@SuppressWarnings({"unchecked"})
@Override
public boolean matches(Method method, Class targetClass) {
Method m = method;
if (targetClass != null) {
try {
m = targetClass.getMethod(m.getName(), m.getParameterTypes());
return this.isFrameAnnotation(m);
} catch (NoSuchMethodException ignored) {

}
}
return super.matches(method, targetClass);
}

private boolean isFrameAnnotation(Method method) {
return null != AnnotationUtils.findAnnotation(method, Auth.class);
}
}

方法拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AuthMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {

public AuthMethodInterceptor() {
super(new AuthHandler());
}

public AuthMethodInterceptor(AnnotationResolver resolver) {
super(new AuthHandler(), resolver);
}

@Override
public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
// 验证权限
try {
((AuthHandler) this.getHandler()).assertAuthorized(getAnnotation(mi));
} catch (AuthorizationException ae) {
if (ae.getCause() == null) {
ae.initCause(new AuthorizationException("当前的方法没有通过鉴权: " + mi.getMethod()));
}
throw ae;
}
}
}

AOP切面拦截器

1
2
3
4
5
6
7
public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor {
public AuthAopInterceptor() {
super();
// 添加自定义的注解拦截器
this.methodInterceptors.add(new AuthMethodInterceptor(new SpringAnnotationResolver()));
}
}

配置shiro

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

@Bean("sessionManager")
public SessionManager sessionManager(RedisShiroSessionDAO redisShiroSessionDAO,
@Value("${it.redis.open}") boolean redisOpen,
@Value("${it.shiro.redis}") boolean shiroRedis){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//设置session过期时间为1小时(单位:毫秒),默认为30分钟
sessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);

//如果开启redis缓存且syd.shiro.redis=true,则shiro session存到redis里
if(redisOpen && shiroRedis){
sessionManager.setSessionDAO(redisShiroSessionDAO);
}
return sessionManager;
}

@Bean("securityManager")
public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
securityManager.setSessionManager(sessionManager);

return securityManager;
}


@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setLoginUrl("/login.html");
shiroFilter.setUnauthorizedUrl("/");

Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/swagger-resources/**", "anon");

filterMap.put("/statics/**", "anon");
filterMap.put("/login.html", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/favicon.ico", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/**", "authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);

return shiroFilter;
}

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

@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}


// 默认注解拦截方式
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}

// 自定义注解拦截,可以在ShiroAdvisor类上加@Component,就不需要这个方法
/**
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new ShiroAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}*/
}

shiro 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
73
74
75
76
77
@Component
public class UserRealm extends AuthorizingRealm {
@Autowired
private SysUserDao sysUserDao;
@Autowired
private SysMenuDao sysMenuDao;

/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
Long userId = user.getUserId();

List<String> permsList;

//系统管理员,拥有最高权限
if(userId == Constant.SUPER_ADMIN){
List<SysMenuEntity> menuList = sysMenuDao.selectList(null);
permsList = new ArrayList<>(menuList.size());
for(SysMenuEntity menu : menuList){
permsList.add(menu.getPerms());
}
}else{
permsList = sysUserDao.queryAllPerms(userId);
}

//用户权限列表
Set<String> permsSet = new HashSet<>();
for(String perms : permsList){
if(StringUtils.isBlank(perms)){
continue;
}
permsSet.addAll(Arrays.asList(perms.trim().split(",")));
}

SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}

/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authcToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authcToken;

//查询用户信息
SysUserEntity user = new SysUserEntity();
user.setAccount(token.getUsername());
user = sysUserDao.selectOne(user);

//账号不存在
if(user == null) {
throw new UnknownAccountException("账号或密码不正确");
}

//账号锁定
if(user.getStatus() == 0){
throw new LockedAccountException("账号已被锁定,请联系管理员");
}

SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
return info;
}

@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
shaCredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName);
shaCredentialsMatcher.setHashIterations(ShiroUtils.hashIterations);
super.setCredentialsMatcher(shaCredentialsMatcher);
}
}

shiro工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ShiroUtils {

/** 加密算法 */
public final static String hashAlgorithmName = "SHA-256";
/** 循环次数 */
public final static int hashIterations = 16;

public static String sha256(String password, String salt) {
return new SimpleHash(hashAlgorithmName, password, salt, hashIterations).toString();
}

public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}

public static Subject getSubject() {
return SecurityUtils.getSubject();
}
...
}

使用demo

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("sys")
public class SysDictController {

@GetMapping("/dict")
@Auth(value={"sys:dict:list"})
public Result dict(@RequestParam Map<String, Object> params){
PageUtils page = sysDictService.queryPage(params);

return Result.ok().put("page", page);
}
}

shiro自定义注解方案二

定义注解

1
2
3
4
5
6
7
8
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PermissionAnnotation {

String permissionName();

}

定义切面

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
@Aspect
@Component
public class PermissionAspect {

@Pointcut("@annotation(vip.infotech.base.common.shiro.PermissionAnnotation)")
private void permisson() {

}

/**
* 给添加PermissionAnnotation注解的方法校验权限,而不必每个方法内都判断权限
*
* @param joinPoint
* @param permissionAnnotation
* @return
* @throws Throwable
*/
@Around("permisson()&&@annotation(permissionAnnotation)")
public Object advice(ProceedingJoinPoint joinPoint, PermissionAnnotation permissionAnnotation) throws Throwable {

Result result = null;
result = (Result) joinPoint.proceed();
String permissionName = permissionAnnotation.permissionName();

if (StringUtils.isEmpty(permissionName)) {
result.put("success", false);
result.put("msg", "权限名称不能为空");
return result;
}

//校验当前登录用户是否有该权限
boolean permissioned = UserUtils.isPermission(permissionName);

if (!permissioned) {
result.put("success", false);
result.put("msg", "权限名称不能为空");
}

return result;
}

}

权限验证工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserUtils {

/**
* 校验当前登录用户是否有该权限
*
* @param permissionname 权限名称
* @return
*/
public static boolean isPermission(String permissionname) {

Subject subject = SecurityUtils.getSubject();

if (subject.isPermitted(permissionname)) {
return true;
} else {
return false;
}
}
}

使用demo

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("sys")
public class SysDictController {

@GetMapping("/dict")
@PermissionAnnotation(permissionName = "sys:dict:list")
public Result dict(@RequestParam Map<String, Object> params){
PageUtils page = sysDictService.queryPage(params);

return Result.ok().put("page", page);
}
}

Session

tomcat禁用session,或者自己控制session。

把spring controller参数里面的HttpServletRequest对象和HttpSession对象打印出来:打印的结果是org.apache.shiro.web.servlet.ShiroHttpServletRequestorg.apache.shiro.web.servlet.ShiroHttpSession

是如何进行封装的?

查看filter调用链:AbstractShiroFilter中实现了doFilterInternal()。

1
2
3
4
5
6
7
8
9
10
11
12
protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) {
ServletRequest toUse = request;
if (request instanceof HttpServletRequest) {
HttpServletRequest http = (HttpServletRequest)request;
toUse = this.wrapServletRequest(http);
}

return toUse;
}
protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
return new ShiroHttpServletRequest(orig, this.getServletContext(), this.isHttpSessions());
}

这就知道request怎么变成ShiroHttpServletRequest的了。

Shiro重写了默认的HttpServletRequestWrapper

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
public class ShiroHttpServletRequest extends HttpServletRequestWrapper {
...
protected boolean httpSessions = true;

public ShiroHttpServletRequest(HttpServletRequest wrapped, ServletContext servletContext, boolean httpSessions) {
super(wrapped);
this.servletContext = servletContext;
this.httpSessions = httpSessions;
}

protected boolean isHttpSessions() {
return this.getSecurityManager().isHttpSessionMode();
}
...
/**
* 如果this.isHttpSessions()返回true,则返回父类HttpServletRequestWrapper的
* 也就是servelet规范的session,否则返回ShiroHttpSession对象
*/
public HttpSession getSession(boolean create) {
HttpSession httpSession;
if (this.isHttpSessions()) {
httpSession = super.getSession(false);
if (httpSession == null && create) {
if (!WebUtils._isSessionCreationEnabled(this)) {
throw this.newNoSessionCreationException();
}

httpSession = super.getSession(create);
}
} else {
if (this.session == null) {
boolean existing = this.getSubject().getSession(false) != null;
Session shiroSession = this.getSubject().getSession(create);
if (shiroSession != null) {
this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
if (!existing) {
this.setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
}
}

httpSession = this.session;
}

return httpSession;
}
}


publi class DelegatingSubject extends Subject {
...
public HttpSession getSession(boolean create) {
if (log.isTraceEnabled()) {
log.trace("attempting to get session; create = " + create + "; session is null = " + (this.session == null) + "; session has id = " + (this.session != null && this.session.getId() != null));
}

if (this.session == null && create) {
if (!this.isSessionCreationEnabled()) {
String msg = "Session creation has been disabled for the current subject. This exception indicates that there is either a programming error (using a session when it should never be used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc " + "for more.";
throw new DisabledSessionException(msg);
}

log.trace("Starting session for host {}", this.getHost());
SessionContext sessionContext = this.createSessionContext();
Session session = this.securityManager.start(sessionContext);
this.session = this.decorate(session);
}

return this.session;
}
...
}

session是如何封装的?

this.getSecurityManager().isHttpSessionMode()取决于SecurityManager

我项目里ShiroConfig中

1
2
3
4
5
6
7
8
@Bean("securityManager")
public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
securityManager.setSessionManager(sessionManager);

return securityManager;
}

使用的是DefaultWebSecurityManager

1
2
3
4
5
6
7
8
9
10
11
12
public class DefaultWebSecurityManager extends DefaultSecurityManager implements WebSecurityManager {
...
public boolean isHttpSessionMode() {
SessionManager sessionManager = this.getSessionManager();
return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions();
}

public boolean isServletContainerSessions() {
return false;
}
...
}

DefaultWebSessionManager中该方法返回的是false,所以用的是ShiroHttpSession。这样通过shiro就可以灵活控制session。

也可以模仿shiro自己重写HttpServletRequestWrapper

  1. 继承HttpServletRequestWrapper 重写getSession()方法
  2. 配置一个过滤器,在过滤器中配置自己的Request类

还有比较原始的方式:
jsp中<%@ page session="false"%> 然后显示获取session,request.getSession(true)request.getSession(false)不会生成Session。

应用

自定义sessionID生成方案

需要修改的地方比较多重写部分方法,同时可以支持在消息头加token字段或者在cookies中传递sessionID标识。
对于想使用cookeis的客户端可以调用validate接口进行登录,后续就可以自动带上cookies进行操作了。
对于需要调用第三方平台进行校验的可以统一拦截请求判断请求头中是否携带token,如果携带的token没通过校验就去校验中心进行校验,校验通过使用token创建对应session设置cookies。

扩展

Spring Cloud微服务权限验证

OAuth2验证

验证流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+--------+                               +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+

所有的Token有效性校验都落在的授权服务器上,服务器压力大。

应用场景:
各大应用内的qq,微信,微博登录等。
a.用户点击qq登录,会先跳转到qq登录页面,这时请求已经跳转到qq服务器了,然后用户输入账号或者扫码登录,这时所有请求都在qq服务器完成
b.用户正确登录后,qq服务器返回用户的code给第三方应用,然后第三方应用再使用code去授权服务器请求获取token。(这一步用户不可见)
c.第三方应用获取到token后,再使用token获取用户的qq名称,头像等信息

优点:用户可以控制自身的一些权限是否给第三方,第三方只能获取到用户临时产生的一个访问的code,安全性。
缺点:认证过程繁琐。

JWT验证

验证流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+--------+                               +---------------+
| | 1-Request Quthorization |Authorization |
| |---------- ------------ ------>| Service |
| |grant_type&username&password | |
| | | |--+
| | 3-Response Authorization | | | 2-Gen JWT
| |< -------- ------------ ------ | Private Key |--+
| | access-token/refresh_token | |
| | token_type/expire_in | |
| Client | +---------------+
| |
| | +---------------+
| | 4-Request Resource | Resource |
| |---------- ------------ ------>| Service |--+
| |Auth:bearer Access Token | | | 5-Verify Token
| | | |--+
| | 6-Response Resource | |
| |< -------- ------------ ------ | Public Key |
+--------+ +---------------+

业务服务器自行校验,降低服务器压力。

适用场景:
一次性的身份认证、api的鉴权等,这些场景能充分发挥jwt无状态以及分布式验证的优势。

不适用的场景:
不要试图用jwt去代替session。这种模式下其实传统的session+cookie机制工作的更好,jwt因为其无状态和分布式,事实上只要在有效期内,是无法作废的,用户的签退更多是一个客户端的签退,服务端token仍然有效,你只要使用这个token,仍然可以登陆系统。另外一个问题是续签问题,使用token,无疑令续签变得十分麻烦,当然你也可以通过redis去记录token状态,并在用户访问后更新这个状态,但这就是硬生生把jwt的无状态搞成有状态了,而这些在传统的session+cookie机制中都是不需要去考虑的。

微服务中Auth2+JWT组合使用

通过JWT生成Token。后端可以通过token获取用户信息。token生成策略除了JWT还有Auth2自己的策略。可以通过配置把Auth2的token存储在内存(单实例)、存储在redis或者存储在其他数据库中。可以通过DefaultTokenServices进行配置
由于JWT无法手动过期,可以JWT跟网关结合,增加存储模块存储过期的Token,实现过期。

微服务中直接用Redis

生成随机Token与对应用户信息存储在redis中,后续请求经过网关服务器进行校验,校验通过在转发请求时在请求头增加用户信息,后端服务可以获取得到用户信息。

其他方案服务间校验

  1. 在Zuul Api网关上进行校验,可以只在zuul网关上进行校验,但是后端服务之间调用不安全,所以可以都进行校验
  2. Service中在Interceptor上进行拦截,去服务鉴权中心获取token校验。这样要频繁访问鉴权中心,可以考虑优化

参考

The OAuth 2.0 Authorization Framework
Oauth2详解-介绍(一)
理解OAuth 2.0
微服务场景使用OAuth2
微服务架构下的统一身份认证和授权