背景介绍
Apache Shiro是Java的一个安全框架。目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。
知识剖析
1.常用的几个概念
Subject
主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前的应用交互的任何东西都是subject,比如网络爬虫,机器人等;即一个抽象概念,所有的subject都绑定到SecurityManager,与subject的所有交互都会委给securityManager;可以把subject认为是一个门面,SecuriManager才是实际的执行者。
SecurityManager
安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器。
Realm
域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
2.身份认证
即在应用中谁能证明他就是他本人。一般提供如他们的身份ID一些标识信息来表明他就是他本人,如提供身份证,用户名/密码来证明。在shiro中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身份。
**principals:**身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
**credentials:**证明/凭证,即只有主体知道的安全值,如密码/数字证书等。最常见的principals和credentials组合就是用户名/密码了。接下来先进行一个基本的身份认证。
3.权限管理
授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
主体
主体,即访问应用的用户,在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。
资源
在应用中用户可以访问的任何东西,比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
权限
安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:
访问用户列表页面
查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)
打印文档等等。
如上可以看出,权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许,不反映谁去执行这个操作。所以后续还需要把权限赋予给用户,即定义哪个用户允许在某个资源上做什么操作(权限),Shiro不会去做这件事情,而是由实现人员提供。
Shiro支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的),后续部分介绍。
角色
角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。
常见问题
Q:基于角色授权的优缺点?
解决方案
A:基于角色授权缺点就是如果很多地方进行了角色判断,但是有一天不需要了那么就需要修改相应代码把所有相关的地方进行删除;这就是粗粒度造成的问题。优点就是控制逻辑较为简单。
编码实战
1.身份认证
步骤一:先导入相关依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
</dependencies>
数据库表
一切从简,用户 user 表,以及角色 role 表
Shiro 相关类
Shiro 配置类
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/notLogin");
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
// 设置拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//游客,开发权限
filterChainDefinitionMap.put("/guest/**", "anon");
//用户,需要角色权限 “user”
filterChainDefinitionMap.put("/user/**", "roles[user]");
//管理员,需要角色权限 “admin”
filterChainDefinitionMap.put("/admin/**", "roles[admin]");
//开放登陆接口
filterChainDefinitionMap.put("/login", "anon");
//其余接口一律拦截
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
System.out.println("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(customRealm());
return securityManager;
}
/**
* 自定义身份认证 realm;
* <p>
* 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
* 否则会影响 CustomRealm类 中其他类的依赖注入
*/
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
}
注意:里面的 SecurityManager 类导入的应该是 import org.apache.shiro.mgt.SecurityManager; 但是,如果你是复制代码过来的话,会默认导入 java.lang.SecurityManager 这里也稍稍有点坑,其他的类的话,也是都属于 shiro 包里面的类
shirFilter 方法中主要是设置了一些重要的跳转 url,比如未登陆时,无权限时的跳转;以及设置了各类 url 的权限拦截,比如 /user 开始的 url 需要 user 权限,/admin 开始的 url 需要 admin 权限等
权限拦截 Filter
当运行一个Web应用程序时,Shiro将会创建一些有用的默认 Filter 实例,并自动地将它们置为可用,而这些默认的 Filter 实例是被 DefaultFilter 枚举类定义的,当然我们也可以自定义 Filter 实例,这些在以后的文章中会讲到
常用的主要就是 anon,authc,user,roles,perms 等
注意:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器(例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到 shiroFilterFactoryBean.setLoginUrl(); 设置的 url )
自定义 realm 类
我们首先要继承 AuthorizingRealm 类来自定义我们自己的 realm 以进行我们自定义的身份,权限认证操作。
记得要 Override 重写 doGetAuthenticationInfo 和 doGetAuthorizationInfo 两个方法(两个方法名很相似,不要搞错)
public class CustomRealm extends AuthorizingRealm {
private UserMapper userMapper;
@Autowired
private void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
/**
* 获取身份验证信息
* Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
*
* @param authenticationToken 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————身份认证方法————");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 从数据库获取对应用户名密码的用户
String password = userMapper.getPassword(token.getUsername());
if (null == password) {
throw new AccountException("用户名不正确");
} else if (!password.equals(new String((char[]) token.getCredentials()))) {
throw new AccountException("密码不正确");
}
return new SimpleAuthenticationInfo(token.getPrincipal(), password, getName());
}
/**
* 获取授权信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("————权限认证————");
String username = (String) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获得该用户角色
String role = userMapper.getRole(username);
Set<String> set = new HashSet<>();
//需要将 role 封装到 Set 作为 info.setRoles() 的参数
set.add(role);
//设置该用户拥有的角色
info.setRoles(set);
return info;
}
}
重写的两个方法分别是实现身份认证以及权限认证,shiro 中有个作登陆操作的 Subject.login() 方法,当我们把封装了用户名,密码的 token 作为参数传入,便会跑进这两个方法里面(不一定两个方法都会进入)
其中 doGetAuthorizationInfo 方法只有在需要权限认证时才会进去,比如前面配置类中配置了 filterChainDefinitionMap.put("/admin/", “roles[admin]”);** 的管理员角色,这时进入 /admin 时就会进入 doGetAuthorizationInfo 方法来检查权限;而 doGetAuthenticationInfo 方法则是需要身份认证时(比如前面的 Subject.login() 方法)才会进入
再说下 UsernamePasswordToken 类,我们可以从该对象拿到登陆时的用户名和密码(登陆时会使用 new UsernamePasswordToken(username, password);),而 get 用户名或密码有以下几个方法
token.getUsername() //获得用户名 String
token.getPrincipal() //获得用户名 Object
token.getPassword() //获得密码 char[]
token.getCredentials() //获得密码 Object
注意:有很多人会发现,UserMapper 等类,接口无法通过 @Autowired 注入进来,跑程序的时候会报 NullPointerException,网上说了很多诸如是 Spring 加载顺序等原因,但其实有一个很重要的地方要大家注意,CustomRealm 这个类是在 shiro 配置类的 securityManager.setRealm() 方法中设置进去的,而很多人直接写securityManager.setRealm(new CustomRealm()); ,这样是不行的,必须要使用 @Bean 注入 MyRealm,不能直接 new 对象:
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(customRealm());
return securityManager;
}
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
道理也很简单,和 Controller 中调用 Service 一样,都是 SpringBean,不能自己 new
当然,同样的道理也可以这样写:
@Bean
public SecurityManager securityManager(CustomRealm customRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(customRealm);
return securityManager;
}
然后只要在 CustomRealm 类加上个类似 @Component 的注解即可
功能实现
本文的功能全部以接口返回 json 数据的方式实现
根据 url 权限分配 controller
游客
@RestController
@RequestMapping("/guest")
public class GuestController{
@Autowired
private final ResultMap resultMap;
@RequestMapping(value = "/enter", method = RequestMethod.GET)
public ResultMap login() {
return resultMap.success().message("欢迎进入,您的身份是游客");
}
@RequestMapping(value = "/getMessage", method = RequestMethod.GET)
public ResultMap submitLogin() {
return resultMap.success().message("您拥有获得该接口的信息的权限!");
}
}
普通登陆用户
@RestController
@RequestMapping("/user")
public class UserController{
@Autowired
private final ResultMap resultMap;
@RequestMapping(value = "/getMessage", method = RequestMethod.GET)
public ResultMap getMessage() {
return resultMap.success().message("您拥有用户权限,可以获得该接口的信息!");
}
}
管理员
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private final ResultMap resultMap;
@RequestMapping(value = "/getMessage", method = RequestMethod.GET)
public ResultMap getMessage() {
return resultMap.success().message("您拥有管理员权限,可以获得该接口的信息!");
}
}
突然注意到 CustomRealm 类那里抛出了 AccountException 异常,现在建个类进行异常捕获
@RestControllerAdvice
public class ExceptionController {
private final ResultMap resultMap;
@Autowired
public ExceptionController(ResultMap resultMap) {
this.resultMap = resultMap;
}
// 捕捉 CustomRealm 抛出的异常
@ExceptionHandler(AccountException.class)
public ResultMap handleShiroException(Exception ex) {
return resultMap.fail().message(ex.getMessage());
}
}
还有进行登陆等处理的 LoginController
@RestController
public class LoginController {
@Autowired
private ResultMap resultMap;
private UserMapper userMapper;
@RequestMapping(value = "/notLogin", method = RequestMethod.GET)
public ResultMap notLogin() {
return resultMap.success().message("您尚未登陆!");
}
@RequestMapping(value = "/notRole", method = RequestMethod.GET)
public ResultMap notRole() {
return resultMap.success().message("您没有权限!");
}
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public ResultMap logout() {
Subject subject = SecurityUtils.getSubject();
//注销
subject.logout();
return resultMap.success().message("成功注销!");
}
/**
* 登陆
*
* @param username 用户名
* @param password 密码
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
public ResultMap login(String username, String password) {
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行认证登陆
subject.login(token);
//根据权限,指定返回数据
String role = userMapper.getRole(username);
if ("user".equals(role)) {
return resultMap.success().message("欢迎登陆");
}
if ("admin".equals(role)) {
return resultMap.success().message("欢迎来到管理员页面");
}
return resultMap.fail().message("权限错误!");
}
}
测试