概述
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、加密和会话管理,使用shiro可以非常方便的完成项目的权限管理模块开发
三个核心组件:
- Subject:当前操作用户,可以是登录用户,也可以是第三方访问程序
- SecurityManager:管理所有用户的安全操作,包括执行身份验证、授权、加密和会话管理
- Realms:当对用户执行认证和授权验证时,Shiro会从应用配置的Realm中查找用户及其权限信息,本质上是一个实现了查询用户权限的DAO
maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.5.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
</dependencies>
Shiro配置
在springboot项目中,相关配置都是用Java代码加配置文件的方式实现,要使用shiro,必须对其进行相关配置,如下配置类
@Configuration
public class ShiroConfig {
/**
* 配置SecurityManager
*/
@Bean
@Autowired
public DefaultWebSecurityManager securityManager(
AuthRealm authRealm, DefaultWebSessionManager sessionManager,
AbstractCacheManager cacheManager) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//设置Realm
manager.setRealm(authRealm);
//设置 sessionManager
manager.setSessionManager(sessionManager);
//设置缓存管理
manager.setCacheManager(cacheManager);
return manager;
}
/**
* 开启aop注解支持需要和advisorAutoProxyCreator方法搭配使用
*/
@Bean
@Autowired
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions) 假设在controller方法上有@RequiresPermissions("123")这个注解,则它和配置文件中/xxx=c_perms[123]等效
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* shiro过滤管理器 后面会详细解释
*/
@Bean
@Autowired
@ConfigurationProperties(prefix = "using.shiro.filter")
public ShiroFilterFactoryBean shiroFilterFactory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 此处的自定义过滤器不能交由Spring管理 重要的事情说三遍
factoryBean.getFilters().put("c_user", new UserFilter());
factoryBean.getFilters().put("c_perms", new PermissionsAuthorizationFilter());
return factoryBean;
}
/**
* 设置session管理器 比如超时时间等
*/
@Bean
@ConfigurationProperties(prefix = "using.shiro.session")
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager manager = new DefaultWebSessionManager();
// 所有的session一定要将id设置到Cookie之中,需要提供有Cookie的操作模版 -->
manager.setSessionIdCookie(this.sessionIdCookie());
manager.setSessionListeners(Arrays.asList(shiroSessionListener));
manager.setSessionDAO(sessionDAO);
return manager;
}
/**
* 设置缓存管理器
*/
@Bean
public AbstractCacheManager cacheManager() {
MemoryConstrainedCacheManager manager = new MemoryConstrainedCacheManager();
return manager;
}
/**
* 设置cookie
*/
@Bean
@ConfigurationProperties(prefix = "using.shiro.cookie.sessionId")
public SimpleCookie sessionIdCookie() {
SimpleCookie cookie = new SimpleCookie();
return cookie;
}
}
上述配置类,需要一些具体的配置项,具体配置项如下yml文件所示
using:
shiro:
filter: #在注入过滤器时会用到
loginUrl: /login.html #登录页面
successUrl: /index.html #登录后的首页
unauthorizedUrl: /index403.html #无权访问的页面
# 必须带'|'以保留换行
# anon表示不需要认证可直接访问。 c_user 表示需要用c_user对应的过滤器去执行,如UserFilter
# factoryBean.getFilters().put("c_user", new UserFilter(sessionManager, userService));
filterChainDefinitions: |
/static/**=anon
/login.html=anon
/index403.html=anon
/login=anon
/logout=c_user
/menu/queryMenu=c_user
session:
# 超时时间 30 分钟
globalSessionTimeout: 1800000
deleteInvalidSessions: true
# 1分钟扫描一次
sessionValidationInterval: 60000
sessionValidationSchedulerEnabled: true
# 定义sessionIdCookie模版可以进行操作的启用
sessionIdCookieEnabled: true
cookie:
sessionId:
# 默认为JSESSIONID
name: token
# 保证该系统不会受到跨域的脚本操作供给, 默认为false
httpOnly: true
# 单位秒,默认为-1表示浏览器关闭,则Cookie消失
maxAge: -1
Shiro的认证
认证:可以理解成登录,即在应用中谁能证明他就是他本人。如提供身份证,用户名来证明。在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,从而应用能验证用户身份:
principals:身份,即主体的标识属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
代码示例
@RestController
public class LogController{
@PostMapping("login")
public String login(@RequestParam("username") String userName,
@RequestParam("password") String password){
//1、得到Subject及创建用户名/密码身份验证Token
Subject subject = SecurityUtils.getSubject();
//这里的token类型(UsernamePasswordToken)要和后面的AuthorizingRealm.doGetAuthenticationInfo方法参数类型保持一致
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
try {
//2、登录,即身份验证 ,并缓存session 及权限等信息
subject.login(token);
} catch (AuthenticationException e) {
//3、身份验证失败
return "登录失败";
}
return "登录成功";
}
@PostMapping("logout")
public String logout() {
//1. 获取用户
Subject subject = SecurityUtils.getSubject();
//2. 退出,并清空session 权限等缓存信息
subject.logout();
return "退出成功";
}
}
Shiro的Realm
Realm获取用户的权限信息
//自定义Realm 继承AuthorizingRealm
@Component
public class AuthRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private DefaultWebSessionManager sessionManager;
/***
* 获取认证信息,在登录的时候会自动调用,根据用户名密码去验证用户
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
//这里的token类型要和登录时的token类型保持一致
if (!(authToken instanceof UsernamePasswordToken)) {
throw new AuthenticationException("未知的验证类型, 目前仅支持用户名密码验证");
}
UsernamePasswordToken pwdToken = (UsernamePasswordToken) authToken;
//根据用户名查询用户
User user = this.userService.getUserByUsername(pwdToken.getUsername());
if (user == null) {
throw new UnknownAccountException();
}
//根据查询出来的用户名和密码存储给AuthenticationInfo类,然后在框架中去验证用户登录密码是否和查询出来的密码一致
//如果一致则登录成功,否则登录失败
AuthenticationInfo info = new SimpleAuthenticationInfo(user.getUserName(), user.getPassword(),
this.getClass().getName());
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
user.setPassword(null);
session.setAttribute("USER_INFO", user);
return info;
}
/***
* 获取权限信息 在登录的时候回自动调用,但必须先执行上面一个方法doGetAuthenticationInfo
* 并将权限信息缓存起来
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户
Subject subject = SecurityUtils.getSubject();
//根据用户获取session
Session session = subject.getSession();
Object sysUserObj = session.getAttribute("USER_INFO");
User userInfo = (User) sysUserObj;
//存放角色信息
Set<String> roleSet = new HashSet<>();
//存放权限集合
Set<String> permissionSet = new HashSet<>();
// 查询用户角色 用自定义的userService查询用户角色具有的资源权限信息
List<Resource> resources = userService.getResourcesByRole(userInfo.getRoleNo());
for (Resource resource : resources) {
// 这里必须和配置文件/xxx=c_perms["RES_ID_100"]或注解@RequiresPermissions("RES_ID_100")保持一致
permissionSet.add("RES_ID_" + resource.getId());
}
//创建返回数据
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//设置角色
authorizationInfo.setRoles(roleSet);
//设置所有资源权限
authorizationInfo.setStringPermissions(permissionSet);
return authorizationInfo;
}
}
认证的执行流程:
- 用户调用登录请求,在登录时调用SecurityUtils.getSubject().login(token)
- shiro调用AuthorizingRealm类的doGetAuthenticationInfo方法进行验证
- 验证通过后,调用AuthorizingRealm类的doGetAuthorizationInfo方法查询该用户具有的权限信息
过滤器
shiro中默认的过滤器
过滤器名称 | 过滤器类 | 备注 |
---|---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon” |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录 |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | Basic HTTP身份验证拦截器,如果不通过,跳转到登录页面 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter | 退出拦截器,示例“/logout=logout” |
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | 不创建会话拦截器,调用 subject.getSession(false)不会有什么问题,但是如果 subject.getSession(true)将抛出 DisabledSessionException异常; |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 权限拦截器,验证用户是否拥有该URL规定的所有权限;示例“/user/**=perms” |
port | org.apache.shiro.web.filter.authz.PortFilter | 端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样 |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | rest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll); |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有该URL规定的所有角色; |
ssl | org.apache.shiro.web.filter.authz.SslFilter | SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样; |
user | org.apache.shiro.web.filter.authc.UserFilter | 用户拦截器,用户身份验证已经/记住我登录的都可;示例“/**=user” |
自定义过滤器
自定义过滤器可继承如下类:
- OncePerRequestFilter:保证一次请求只调用一次doFilterInternal,即如内部的forward不会再多执行一次doFilterInternal
- AdviceFilter:AdviceFilter提供了AOP的功能,其实现和SpringMVC中的Interceptor思想一样
- PathMatchingFilter:PathMatchingFilter继承了AdviceFilter,提供了url模式过滤的功能,如果需要对指定的请求进行处理,可以扩展PathMatchingFilter
- AccessControlFilter(常用):继承了PathMatchingFilter,并扩展了两个方法,isAccessAllowed和onAccessDenied
- UserFilter:继承AccessControlFilter,表示基于用户是否登录的拦截器,可以直接使用
- RolesAuthorizationFilter:继承AccessControlFilter,表示基于特殊角色的拦截过滤,可以直接使用
- PermissionsAuthorizationFilter(常用):继承AccessControlFilter,验证某个URL请求,是否拥有访问权限,可以直接使用
要使自定义过滤器生效,必须执行如下步骤:
- 自定义过滤器
- 在拦截器工厂里注册过滤器,如factoryBean.getFilters().put(“c_once”, new MyOncePerRequestFilter());
- 设置URL的过滤器链,如factoryBean.getFilterChainDefinitionMap().put("/once/**", “c_once”)
一般上述2 3步都是在配置类中进行指定,即自定义过滤器不能交由spring管理,切记切记切记
OncePerRequestFilter
保证一次请求只调用一次doFilterInternal,即如内部的forward不会再多执行一次doFilterInternal
public class MyOncePerRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
System.out.println("=========once per request filter");
chain.doFilter(request, response);
}
}
AdviceFilter
AdviceFilter提供了AOP的功能,其实现和SpringMVC中的拦截器思想一样
public class MyAdviceFilter extends AdviceFilter {
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("前置处理,返回false将中断后续拦截器的执行");
return true;
}
@Override
protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("执行完拦截器链之后正常返回后执行");
}
@Override
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
System.out.println("后置最终处理,不管正常结束还是异常结束都会进入本方法");
}
}
PathMatchingFilter
PathMatchingFilter继承了AdviceFilter,提供了url模式过滤的功能,如果需要对指定的请求进行处理,可以扩展PathMatchingFilter
public class MyPathMatchingFilter extends PathMatchingFilter {
//当调用preHandle方法时,会回调如下方法
@Override
protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
System.out.println("url matches,config is " + Arrays.toString((String[])mappedValue));
return true;
}
}
AccessControlFilter
继承了PathMatchingFilter,并扩展了两个方法:
- isAccessAllowed:是否允许访问,返回true表示允许
- onAccessDenied:访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了
public class MyAccessControlFilter extends AccessControlFilter {
//是否允许访问,返回true表示允许
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
System.out.println("access allowed");
return true;
}
//访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("访问拒绝也不自己处理,继续拦截器链的执行");
return true;
}
}
UserFilter
继承AccessControlFilter,表示基于用户登录的拦截器,即判断用户是否登录。一般情况下这个类可以直接使用。
需求:管理员可以修改用户的状态,比如开通和停用,假设某个用户状态为开通并登录系统了,这时管理员更新状态为停用,
按照jar包提供的UserFilter类,那么只要这个用户不退出,仍然可以使用系统。现在的需求是,只要管理员更新状态为停用,
则登录用户访问系统则需要被强制退出
public class MyUserFilter extends UserFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(UserFilter.class);
private DefaultWebSessionManager sessionManager;
private UserService userService;
public UserFilter(DefaultWebSessionManager sessionManager, UserService userService) {
this.sessionManager = sessionManager;
this.userService = userService;
}
/**
* 是否允许访问,返回true表示允许
* 父类的方法意思是:如果是登录用户,则允许访问,否则不允许访问
* 假设需求为:管理员可以修改用户的状态,比如开通和停用,
* 假设原本开通的用户,管理员更新状态为停用,且状态更新后登录用户需要被退出
*
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse,
Object mappedValue) {
boolean allowed = super.isAccessAllowed(servletRequest, servletResponse, mappedValue);
if (allowed && !isLoginRequest(servletRequest, servletResponse)) {
Subject subject = getSubject(servletRequest, servletResponse);
Session session = subject.getSession();
String username = subject.getPrincipal().toString();
user user = this.userService.getUserByUsername(username);
// 检测用户状态
if (user == null || user.getState() != SysUserConstant.STATE_NORMAL) {
subject.logout();
this.sessionManager.getSessionIdCookie().setValue(Cookie.DELETED_COOKIE_VALUE);
return false;
}
}
return allowed;
}
/**
* 访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了
* 父类的方法意思是:重定向到登录页面,没有提示信息
* 假设需求为:所有请求都是ajax请求,并返回一个自定义的类,该类有相关提示信息
*
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
boolean isAjax = RequestUtils.isAjax(request);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
if (isAjax) {
JsonResult<String> jsonResult = new JsonResult<>();
jsonResult.setErrorMessage(ErrorEnum.AUTH_NOT_LOGIN);
response.getWriter().write(JSONObject.toJSONString(jsonResult));
return false;
} else {
return super.onAccessDenied(servletRequest, servletResponse);
}
}
}
过滤器链
如果一个URL只有一个过滤器,那么这一个过滤器就是一个链,如果为一个URL配置多个过滤器时,那么这多个过滤器组成一个过滤器链,链中的过滤器的处理顺序则和配置中的先后顺序一致。
需求:假设需要先验证用户是否登录,再验证用户是否有这个权限,则有如下2种处理方式:
- 使用一个过滤器,先验证是否登录再验证是否有权限,配置为/api/**=c_login_and_perms
- 使用两个过滤器,一个验证是否登录,一个验证是否有权限,配置为/api/**=c_login,c_perms,则先执行c_login对应的过滤器,再执行c_perms对应的过滤器
重新生成过滤器链
为什么需要我们重新生成过滤器链?
我们以shiro框架提供的UserFilter和PermissionsAuthorizationFilter为例进行说明。
一般情况下我们的配置文件如下:
shiro:
filter: #在注入过滤器时会用到
loginUrl: /login.html #登录页面
successUrl: /index.html #登录后的首页
unauthorizedUrl: /index403.html #无权访问的403页面
# 必须带'|'以保留换行
# anon表示不需要认证可直接访问。
# c_user 表示需要用c_user对应的过滤器去执行,如框架自带的UserFilter,需要在配置类的factorybean中进行配置
# factoryBean.getFilters().put("c_user", new UserFilter());
# c_perms 表示需要用c_perms对应的过滤器去执行,如框架自带的PermissionsAuthorizationFilter,也需要在配置类中配置
# factoryBean.getFilters().put("c_perms", new 如框架自带的PermissionsAuthorizationFilter());
# /**=c_perms 配置中千万不要出现这个,重要的事说三遍,
# 原因是所有请求在isAccessAllowed方法中,因为mappedValue参数为null,会直接返回true,达不到权限校验的效果,详细原因见后面的说明
filterChainDefinitions: |
/static/**=anon
/login.html=anon
/index403.html=anon
/login=anon
/logout=c_user
/menu/queryMenu=c_user
/**=c_user,c_perms
先说结论:上述配置文件中,/**=c_user,c_perms,如果想这样配置会出问题。为什么?
在方法public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)中,mappedValue参数是什么,有什么用?
如果配置为/hello=c_perms[“helloworld”],那么在初始化类PermissionsAuthorizationFilter(UserFilter以及所有继承于PathMatchingFilter的子类都一样)时,
在父类PathMatchingFilter中有一个Map属性appliedPaths,这个map的key为配置中的/hello,value为helloworld。
isAccessAllowed方法的mappedValue参数就是appliedPaths中匹配请求URL对应的value,如果请求匹配到了/hello,那么mappedValue的值就为helloworld。
如果配置为/hello=c_perms,请求匹配到了/hello时,isAccessAllowed方法的mappedValue的值null。
在框架中提供的UserFilter类的isAccessAllowed方法,本身不关注mappedValue参数,因此可以直接配置为/hello=c_user,
但在框架提供的PermissionsAuthorizationFilter类的isAccessAllowed方法中,使用到了mappedValue参数,并将其作为权限进行权限校验,
如果mappedValue参数为空,则直接返回true表示验证通过,如果mappedValue参数不为空,则获取到值去shiro框架进行权限校验,
因此不能简单的配置为/hello=c_perms,而需要配置为/hello=c_perms[xxx],但一个系统中的请求URL很多,我们不能都配置到配置文件中吧,
怎么办呢?
这就是为什么要重新生成过滤器链的原因。
在shiro框架获取到shiro配置后,springboot系统启动完成前,我们需要从数据库把所有URL读取出来,并组装成/url=c_perms[xxx]的形式,最后重新写入过滤器链
具体代码如下:
@Component
public class ShiroFilterManger {
@Autowired
private ShiroFilterFactoryBean shiroFilterFactoryBean;
@Autowired
private UserService userService;
@PostConstruct
public synchronized void reloadFilterChainDefinition() throws Exception {
// 获取预置的过滤器链
Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
Map<String, String> newFilterChainDefinitionMap = new LinkedHashMap<>();
newFilterChainDefinitionMap.putAll(filterChainDefinitionMap);
// 查询数据库所有资源
List<Resource> resources = userService.getAllResources();
for (Resource resource : resources) {
newFilterChainDefinitionMap.put(resource.getResourceUrl(),"c_user,c_perms[\"" + "RES_ID_"+resource.getId() + "\"]");
}
// 重新生成过滤器链
AbstractShiroFilter shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
PathMatchingFilterChainResolver chainResolver = (PathMatchingFilterChainResolver) shiroFilter
.getFilterChainResolver();
DefaultFilterChainManager chainManager = (DefaultFilterChainManager) chainResolver.getFilterChainManager();
chainManager.getFilterChains().clear();
newFilterChainDefinitionMap.forEach((linkUrl, permission) -> {
chainManager.createChain(linkUrl, permission);
});
}
}