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(com.anktion.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。

扩展

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有效性校验都落在的授权服务器上,服务器压力大。

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 |
+--------+ +---------------+

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

其他方案服务间校验

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

参考

The OAuth 2.0 Authorization Framework