笔者在学习了张开涛老师讲解的shiro课程后,对其述内容做了自己的总结。由于张开涛–跟我学shiro一文中利用shiro配置文件简单的写了一个小Demo,所以笔者结合自己的理解采用springboot+mybatis+shiro的框写了一个单系统下、前后端分离的Demo。如有理解错误的地方,欢迎大家指正。
shiro是Apache旗下提供的一套JAVA安全框架,主要用于权限校验。其实spring也提供了一套权限架构即:Spring Security。在实际开发项目中,shiro所提供的功能就可满足绝大部分的需求。Spring Security虽然功能强大,但和spring的耦合度太高,而且其暴露出的用于权限的API没有shiro所提供的的简单明了。所以现在大部分的权限项目都是采用shiro来完成的。
1、Subject:主体,即当前和程序进行交互的角色。可以是用户,也可以是线程。所有的Subject都绑定到SecurityManager组组件中。
2、SecurityManager:安全管理器,即权限校验的核心组件。所有与权限校验有关的操作都是通过它所提供的API来完成,是shiro的核心。相当于springMVC中的前端控制器。
3、Realm:域,即安全数据源。它给SecurityManager提供权限数据的注入,告诉安全管理器哪些用户拥有哪些角色和权限。相当于在shiro配置文件中定义的数据。
由图可以应用代码和Subject组件进行交互,它是shiro对外提供API的核心。然后Subject将权限校验委托给SecurityManager,由安全管理器对当前用户是否有权限操控资源进行校验。最后SecurityMananger向Realm进行询问,Realm告知SecurityManager用户拥有的角色和权限。
1、Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
2、Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
3、Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信
息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
4、Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
5、Web Support:Web 支持,可以非常容易的集成到 Web 环境;
6、Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以
提高效率;
7、Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能
把权限自动传播过去;
8、Testing:提供测试支持;
9、Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
10、Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录
了。
demo中只是简单的实现了登陆以及角色权限的校验。后续会在写shiro的其他功能
<!-- shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
笔者这里采用了lombok插件,使用注解简略了set、get方法。如果不会lombok的使用,自行百度,本篇不做赘述。
用户类
package com.example.pojo;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.List;
/**
* 用户表
*/
@Data
@Table(name = "sys_users")
public class SysUsers {
/**
* 编号
*/
@Id
@Column(name = "id")
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 用户名
*/
@Column(name = "username")
private String username;
/**
* 手机号
*/
@Column(name = "phone")
private String phone;
/**
* 密码
*/
@Column(name = "`PASSWORD`")
private String password;
/**
* 盐值
*/
@Column(name = "salt")
private String salt;
/**
* 头像
*/
@Column(name = "icon")
private String icon;
/**
* 住址
*/
@Column(name = "address")
private String address;
/**
* 状态
*/
@Column(name = "locaed")
private String locaed;
/**
* 用户对应的角色集合 一个用户对应多种角色 这里采用的实体类封装,也可以通过xml映射文件进行Map封装
*/
private List<SysRoles> roles;
}
用户角色类
package com.example.pojo;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.List;
/**
* 角色表
*/
@Data
@Table(name = "sys_roles")
public class SysRoles {
/**
* 角色编号
*/
@Id
@Column(name = "id")
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 角色名称
*/
@Column(name = "rolename")
private String rolename;
/**
* 角色描述
*/
@Column(name = "description")
private String description;
/**
* 状态
*/
@Column(name = "available")
private String available;
/**
* 角色对应的权限集合
*/
private List<SysPermissions> permissions;
}
角色权限类
package com.example.pojo;
import javax.persistence.*;
import lombok.Data;
/**
* 权限表
*/
@Data
@Table(name = "sys_permissions")
public class SysPermissions {
/**
* 编号
*/
@Id
@Column(name = "id")
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 权限名称
*/
@Column(name = "`NAME`")
private String name;
/**
* 权限路径
*/
@Column(name = "url")
private String url;
/**
* 权限类型
*/
@Column(name = "`TYPE`")
private String type;
/**
* 父节点
*/
@Column(name = "pid")
private Integer pid;
/**
* 状态
*/
@Column(name = "available")
private String available;
}
用户角色权限查询方法
其中的service、sevice的实现这里不在展示,自行生成一个就行,主要是为了查询出当前登录用户拥有的角色和权限
<!--查询用户所拥有的角色以及权限-->
<select id="findUserByName" parameterType="String" resultMap="shiroSelect">
select a.*,e.rolename,d.NAME from sys_users a
LEFT JOIN sys_user_role b on a.id=b.user_id
LEFT JOIN sys_role_permission c on b.role_id=c.role_id
LEFT JOIN sys_permissions d on c.permission_id=d.id
LEFT JOIN sys_roles e on b.role_id=e.id
where a.username=#{
userName}
GROUP BY a.username,e.rolename,d.NAME
</select>
<resultMap id="shiroSelect" type="com.example.pojo.SysUsers">
<id column="username" property="username" />
<result column="PASSWORD" property="password" />
<result column="salt" property="salt" />
<collection property="roles" ofType="com.example.pojo.SysRoles">
<id column="rolename" property="rolename" />
<collection property="permissions" ofType="com.example.pojo.SysPermissions">
<id column="NAME" property="name" />
</collection>
</collection>
</resultMap>
/**
* @Author 张佳奇
* @Description
* @Date 2020-04-26 15:53
*/
public class PasswordHelper {
private RandomNumberGenerator randomNumber=new SecureRandomNumberGenerator();
//散列算法 即密码加密
public static final String ALGORITHE_NAME="md5";
//自定义散列次数(加密几次)
public static final int HASH_ITERATIONS=2;
public void emcryptPassword(SysUsers user){
//随机字符串作为盐值因子
user.setSalt(randomNumber.nextBytes().toHex());
String newPassword=new SimpleHash(ALGORITHE_NAME,user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),HASH_ITERATIONS).toHex();
user.setPassword(newPassword);
}
}
package com.example.comment;
import com.example.pojo.SysPermissions;
import com.example.pojo.SysRoles;
import com.example.pojo.SysUsers;
import com.example.service.SysUsersService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import javax.annotation.Resource;
/**
* @Author 张佳奇
* @Description 自定义shiro的核心 为当前用户做认证即赋权
* @Date 2020-04-26 16:08
*/
public class RealmShiro extends AuthorizingRealm {
@Resource
SysUsersService sysUsersService;
/**
* 该方法用于当前用户授权(对当前用户进行角色和权限初始化)[告知shiro当前登录用户所拥有的角色和权限]
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//创建权限信息对象
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
//获取当前用户的用户名
String username=(String)principalCollection.getPrimaryPrincipal();
//根据用户名再数据库查找用户
SysUsers user=sysUsersService.findUserByName(username);
//遍历用户所拥有的角色
for (SysRoles sysRoles:user.getRoles()){
//把用户所拥有的角色加入Realm中
authorizationInfo.addRole(sysRoles.getRolename());
//遍历角色所拥有的权限
for (SysPermissions permissions:sysRoles.getPermissions()){
//把角色所拥有的权限加入Realm中
authorizationInfo.addStringPermission(permissions.getName());
}
}
return authorizationInfo;
}
/**
* 身份认证 登录时判断用户身份信息 做权限校验时,若果没有某种权限则会在此处抛出相应的异常,由下面全局异常捕获到做相应的处理
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//authenticationToken保存了用户的登录信息
//获取当前登录用户名
String username=(String)authenticationToken.getPrincipal();
//根据用户名在数据库中查找用户对应的角色和权限(已在bean对象中接收)
SysUsers sysUsers=sysUsersService.findUserByName(username);
if (sysUsers==null){
return null;
}
//传入的参数分别为用户名,数据库查询到的加密后的密码,该账号密码加密使用的盐值,当前Realm的名称
//返回的SimpleAuthenticationInfo对象中包含各种信息,其中账号密码的校验由shiro自动完成
//返回对象里的数据可以DEBUG跑程序看一下,有助于理解shiro的登录验证以及权限校验
SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(sysUsers.getUsername(),sysUsers.getPassword(), ByteSource.Util.bytes(sysUsers.getSalt()),getName());
return authenticationInfo;
}
@Override
public void setName(String name) {
super.setName("realmShiro");
}
}
package com.example.config;
import com.example.comment.PasswordHelper;
import com.example.comment.RealmShiro;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @Author 张佳奇
* @Description shiro配置
* @Date 2020-04-26 16:35
*/
@Configuration
public class ShiroConfig {
/**
* 开启aop注解支持 下面这两个必须 如若不加则注解不生效
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
/**
* 该方法用于过滤器配置用户权限的url 若采用注解可以不做设置 笔者这里只是写了但是没有用到
* @param securityManager shiro的核心组件,可以看做前端控制器,负责和其他的组件进行交互,所有安全有关的操作都和它有关
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String,String> filterChainDefinitionMap=new HashMap<>();
//如若用户没有登陆就访问了某资源则请求跳转到login请求
shiroFilterFactoryBean.setLoginUrl("/login");
//用户没有某个权限则走unauthc请求
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthc");
//登陆成功后走/home/index请求
shiroFilterFactoryBean.setSuccessUrl("/home");
//filterChainDefinitionMap 增加其他过滤器
return shiroFilterFactoryBean;
}
/**
* 配置加密方式(密码加密)
* @return 返回加密后的密码
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 加密方式(MD5加密)
hashedCredentialsMatcher.setHashAlgorithmName(PasswordHelper.ALGORITHE_NAME);
// 散列次数(2次)
hashedCredentialsMatcher.setHashIterations(PasswordHelper.HASH_ITERATIONS);
return hashedCredentialsMatcher;
}
/**
* 配置Realm
* @return
*/
@Bean
public RealmShiro shiroRealm() {
RealmShiro shiroRealm = new RealmShiro();
// 调用上面方法设置加密方式
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
/**
* 配置安全管理器(核心组件)
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//将角色权限初始化到管理器中 即上面配置的Realm安全数据源
securityManager.setRealm(shiroRealm());
return securityManager;
}
@Bean
public PasswordHelper passwordHelper() {
return new PasswordHelper();
}
}
package com.example.controller;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @Author 张佳奇
* @Description shiro权限验证的全局捕获异常
* @Date 2020-04-28 14:25
*/
@RestControllerAdvice
public class GlobalExceptionAdvice extends Exception{
/**
* 捕获全局异常 简单的考虑了几种异常若有有其他需要 ,请查看shiro异常类型 然后自己增加判断 如若有其他业务异常也可在此增加
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public String Exception(Exception e){
String msg="";
//判断抛出的异常类是否为某个异常的实例
if (e instanceof UnauthorizedException){
msg+="你没有权限";
return msg;
}else if (e instanceof UnknownAccountException){
//一般显示账号或密码错误
msg+="账号不存在";
return msg;
}else if(e instanceof UnauthenticatedException) {
msg+="请登录后访问";
return msg;
}else if (e instanceof IncorrectCredentialsException){
//一般显示账号或密码错误
msg+="密码不匹配";
}
e.printStackTrace();
return msg;
}
}
package com.example.controller;
import com.example.comment.PasswordHelper;
import com.example.pojo.SysUsers;
import com.example.service.SysUsersService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @Author 张佳奇
* @Description 该类为项目的入口类 不需要经过权限验证就可以访问的资源
* @Date 2020-04-26 09:22
*/
@RestController
public class ShiroController {
@Resource
SysUsersService sysUsersService;
@Autowired
private PasswordHelper passwordHelper;
@GetMapping("doLogin")
public Object doLogin(@RequestParam String username, @RequestParam String password) {
//创建用户名/密码验证token
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
System.out.println(token.toString());
//获取当前登录用户
Subject subject = SecurityUtils.getSubject();
//执行登录
//调用该方法时会自动调用comment包中的RealmShiro中的doGetAuthenticationInfo方法
//本来该方法是需要捕获异常的,但由于采用的注解形式进行了全局的异常处理所以在此处不用进行捕获异常
subject.login(token);
SysUsers user = sysUsersService.findUserByName(username);
subject.getSession().setAttribute("user", user);
return "LOGIN SUCCESS";
}
@GetMapping("register")
public Object register(String username,String password) {
SysUsers user = new SysUsers();
user.setUsername(username);
user.setPassword(password);
//该方法将用户密码进行加密存储
passwordHelper.emcryptPassword(user);
//注册成功保存
sysUsersService.saveUser(user);
return "REGISTER SUCCESS";
}
}
package com.example.controller;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author 张佳奇
* @Description 该类需要有一定的权限才可以访问的资源,采用注解模式来判断权限校验
* 采用注解校验权限时会产生异常信息 根据对应的异常信息进行不同的操作 异常处理在GlobalExceptionAdvice中
* @Date 2020-04-28 09:49
*/
@RestController
@RequestMapping("authc")
public class ShiroAuthcController{
/**
* 注解校验角色和权限
*/
@RequiresPermissions("系统设置")
@RequestMapping("renewable")
public String shiroTest(){
return "用户权限校验成功";
}
@RequiresAuthentication
@RequestMapping("/index")
public String shiroT(){
return "登录后才可以访问的资源";
}
@RequiresRoles("超级管理")
@RequestMapping("realms")
public String realm(){
return "用户角色权限校验成功";
}
}
shiro的几个注解的使用及含义如果不懂自行百度 手动狗头护体
至此全部准备完毕,可以通过postman或者页面调用接口观察效果,下面附带几张结果图
最后附加个人项目地址springboot+shiro实现注解权限校验
对于shiro中必要的解释都在注释中写到了,欢迎大佬指正我错误的理解。