概述
Apache Shiro 是 Java 的一个安全框架,它提供了包括认证、授权、加密、会话管理等功能。因为它轻量,集成相对简单,相对于 Spring Security而言,可能没有 Spring Security 做得功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。目前,使用 Apache Shiro 的人越来越多,对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
Shiro 的整体框架大致如下
-
Authentication(认证):用户身份识别,通常被称为用户“登录”。
-
Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
-
Session Management(会话管理):特定于用户的会话管理,甚至在非web 应用程序。
-
Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
-
Web Support:Web 支持,可以非常容易的集成到 Web 环境;
-
Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
-
Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
-
Testing:提供测试支持;
-
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
-
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
图片来自互联网
从应用程序角度的来看Shiro的执行流程
Shiro 架构包含三个主要的理念:Subject, SecurityManager 和 Realm -
Subject:代表当前用户,Subject 可以是一个人,也可以是第三方服务、守护进程帐户、时钟守护任务或者其它当前和软件交互的任何事件。
-
SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
-
Realms:用于进行权限信息的验证,我们自己实现。
Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。
在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
※更多关于shiro的描述,可查阅w3cschool或百度。
在SpringBoot项目中集成Shiro
使用第08节jpa的项目为基础
用到的表sys_admin_user、sys_rol,sys_menu三个
- 在pom.xml文件中加入shiro依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
2.创建三个实体类SysAdminUser.java、SysRole.java、SysMenu.java,主要关注里边的关联查询
package com.zhlab.demo.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@Entity // @Entity: 实体类, 必须
@Table(name="sys_admin_user") //指定表名称
public class SysAdminUser implements Serializable {
private static final long serialVersionUID = 5935904927954268729L;
@Id // @Id: 指明id列, 必须
@GeneratedValue(strategy = GenerationType.IDENTITY) // @GeneratedValue: 表明是否自动生成, 必须, strategy也是必写, 指明主键生成策略, 默认是Oracle
private Long adminUserId;
@Column(name = "user_name", nullable = false)
private String userName;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "nick_name", nullable = false)
private String nickName;
@Column(name = "dept_id")
private Long deptId;
private String phone;
private String email;
private String avatar;
private Boolean status;
private Boolean deletedFlag;
private String loginIp;
private Date loginTime;
private Date createdAt;
private Date updatedAt;
private Date deletedAt;
private Long createdBy;
private Long updatedBy;
private Long deletedBy;
private String remark;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "sys_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
@JsonIgnore
private Set<SysRole> roles = new HashSet<>(0);
//...省略getter、setter
}
package com.zhlab.demo.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@Entity // @Entity: 实体类, 必须
@Table(name="sys_role") //指定表名称
public class SysRole implements Serializable {
private static final long serialVersionUID = 5386313756021968961L;
@Id
@Column(name="role_id")
@GeneratedValue(strategy = GenerationType.IDENTITY) // @GeneratedValue: 表明是否自动生成, 必须, strategy也是必写, 指明主键生成策略, 默认是Oracle
private Long roleID;
@Column(name = "role_name", nullable = false)
private String roleName;
private Integer sort;
private Boolean status;
private Boolean deletedFlag;
private Date createdAt;
private Date updatedAt;
private Date deletedAt;
private Long createdBy;
private Long updatedBy;
private Long deletedBy;
private String remark;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "sys_role_menu",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "menu_id"))
@JsonIgnore
private Set<SysMenu> menus = new HashSet<>(0);
//...省略getter、setter
}
package com.zhlab.demo.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import javax.persistence.criteria.CriteriaBuilder;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@Entity // @Entity: 实体类, 必须
@Table(name="sys_menu") //指定表名称
public class SysMenu implements Serializable {
private static final long serialVersionUID = 4096992355334487966L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // @GeneratedValue: 表明是否自动生成, 必须, strategy也是必写, 指明主键生成策略, 默认是Oracle
private Long menuId;
@Column(name = "menu_name", nullable = false)
private String menuName;
private Long parentId;
private String menuUrl;
private String perms;
private Integer sort;
private Boolean isView;
private Boolean isLink;
private Boolean status;
private Integer menuType;
@ManyToMany(mappedBy = "menus")
@JsonIgnore
private Set<SysRole> roles = new HashSet<>(0);
//...省略getter、setter
}
- 在DAO层创建三个SysAdminUserRepository.java、SysRoleRepository.java、SysMenuRepository.java
都继承JpaRepository接口
4.在Service层创建SysAdminUserService.java、SysRoleService.java处理业务逻辑
package com.zhlab.demo.service;
import com.zhlab.demo.dao.SysAdminUserRepository;
import com.zhlab.demo.model.SysAdminUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @ClassName SysAdminUserService
* @Description //SysAdminUserService
* @Author singleZhang
* @Email 405780096@qq.com
* @Date 2020/10/31 0031 上午 9:45
**/
@Service
public class SysAdminUserService {
@Autowired
SysAdminUserRepository sysAdminUserRepository;
public SysAdminUser getByUserName(String username) {
return sysAdminUserRepository.findByUserName(username);
}
public SysAdminUser save(SysAdminUser user){
return sysAdminUserRepository.save(user);
}
}
package com.zhlab.demo.service;
import com.zhlab.demo.dao.SysRoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @ClassName SysRoleService
* @Description //SysRoleService
* @Author singleZhang
* @Email 405780096@qq.com
* @Date 2020/11/5 0005 上午 10:37
**/
@Service
public class SysRoleService {
@Autowired
private SysRoleRepository sysRoleRepository;
/**
* 判断指定的用户是否存在角色
* @param id 用户ID
*/
public Boolean existsUserOk(Long id) {
return sysRoleRepository.existsByAdminUserId(id);
}
}
- 接口层创建LoginController.java、UserController.java,添加登录和用户添加、编辑的接口,来验证逻辑
package com.zhlab.demo.controller;
import com.zhlab.demo.model.SysAdminUser;
import com.zhlab.demo.service.SysRoleService;
import com.zhlab.demo.shiro.ShiroUtil;
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.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName LoginController
* @Description //登录接口
* @Author singleZhang
* @Email 405780096@qq.com
* @Date 2020/11/4 0004 下午 5:15
**/
@RestController
public class LoginController {
@Autowired
private SysRoleService sysRoleService;
/**
* 登录接口
* */
@PostMapping(value = "/login")
public String login(@RequestBody SysAdminUser user) {
// 添加用户认证信息
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword());
//获取Subject主体对象
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
// 判断是否拥有后台角色
SysAdminUser sysAdminUser = ShiroUtil.getSubject();
if (sysRoleService.existsUserOk(sysAdminUser.getAdminUserId())) {
return "登录成功";
} else {
SecurityUtils.getSubject().logout();
return "您不是后台管理员";
}
} catch (Exception e){
e.printStackTrace();
return "用户名或密码错误";
}
}
/**
* 登出
* */
@GetMapping(value = "/logout")
public String logout() {
return "logout";
}
/**
* 为授权页面
* noAuth
*/
@GetMapping(value = "/noAuth")
public String noAuth() {
return "noAuth";
}
}
package com.zhlab.demo.controller;
import com.zhlab.demo.model.SysAdminUser;
import com.zhlab.demo.service.SysAdminUserService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @ClassName UserController
* @Description //用户接口层
* @Author singleZhang
* @Email 405780096@qq.com
* @Date 2020/10/31 0031 上午 9:43
**/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
SysAdminUserService sysAdminUserService;
/**
* 添加用户
* @param user
* @return
*/
@PostMapping(value = "/add")
public String addUser(@RequestBody SysAdminUser user) {
user = sysAdminUserService.save(user);
return "addUser is ok! \n" + user.toString();
}
/**
* 需要有admin角色才能处理
* */
@RequiresRoles("admin")
@RequiresPermissions("edit")
@GetMapping(value = "/edit")
public String edit() {
return "edit user success!";
}
}
好了,做完上述的步骤,接下来要配置Shiro了
- 创建包com.zhlab.demo.shiro;然后创建AuthRealm.java,需要继承AuthorizingRealm,并重写以下两个方法:
- doGetAuthenticationInfo : 认证逻辑
- doGetAuthorizationInfo : 授权逻辑
package com.zhlab.demo.shiro;
import com.zhlab.demo.model.SysAdminUser;
import com.zhlab.demo.model.SysRole;
import com.zhlab.demo.service.SysAdminUserService;
import org.apache.shiro.authc.*;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import java.util.Set;
/**
* @ClassName AuthRealm
* @Description //验证授权逻辑
* @Author singleZhang
* @Email 405780096@qq.com
* @Date 2020/11/5 0005 上午 10:33
**/
public class AuthRealm extends AuthorizingRealm {
@Autowired
private SysAdminUserService userService;
/**
* 认证逻辑
* 实现用户认证,通过服务加载用户信息并构造认证对象返回
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
//获取用户信息
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String name = token.getPrincipal().toString();
SysAdminUser user = userService.getByUserName(name);
if (user == null) return null;
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,
user.getPassword(), getName());
return info;
}
/**
* 授权逻辑
* 实现权限认证,通过服务加载用户角色和权限信息设置进去
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 获取获取登录用户Principal对象
SysAdminUser user = (SysAdminUser) principal.getPrimaryPrincipal();
// 管理员拥有所有权限
if (user.getAdminUserId().equals(1L)) {
info.addRole("admin");
info.addStringPermission("*:*:*");
return info;
}
// 赋予角色和资源授权
Set<SysRole> roles = ShiroUtil.getSubjectRoles();
roles.forEach(role -> {
info.addRole(role.getRoleName());
role.getMenus().forEach(menu -> {
String perms = menu.getPerms();
if (menu.getStatus() && !StringUtils.isEmpty(perms) && !perms.contains("*")) {
info.addStringPermission(perms);
}
});
});
return info;
}
}
同时在此包下创建一个工具类,ShiroUtil.java,方便获取和操作Shiro中的对象
package com.zhlab.demo.shiro;
import com.zhlab.demo.model.SysAdminUser;
import com.zhlab.demo.model.SysRole;
import com.zhlab.demo.service.SysRoleService;
import org.apache.shiro.SecurityUtils;
import org.hibernate.Hibernate;
import org.hibernate.LazyInitializationException;
import org.springframework.beans.BeanUtils;
import java.util.Set;
/**
* @ClassName ShiroUtil
* @Description //TODO
* @Author singleZhang
* @Email 405780096@qq.com
* @Date 2020/11/5 0005 上午 10:33
**/
public class ShiroUtil {
/**
* 获取当前用户角色列表
*/
public static Set<SysRole> getSubjectRoles() {
SysAdminUser user = (SysAdminUser) SecurityUtils.getSubject().getPrincipal();
// 如果用户为空,则返回空列表
if (user == null) {
user = new SysAdminUser();
}
return user.getRoles();
}
/**
* 获取当前用户对象
*/
public static SysAdminUser getSubject() {
SysAdminUser user = (SysAdminUser) SecurityUtils.getSubject().getPrincipal();
return user;
}
}
- 创建包com.zhlab.demo.shiro.config;然后创建ShiroConfig.java配置类,注入自定义的AuthRealm
package com.zhlab.demo.shiro.config;
import com.zhlab.demo.shiro.AuthRealm;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
/**
* @ClassName ShiroConfig
* @Description //Shiro配置
* @Author singleZhang
* @Email 405780096@qq.com
* @Date 2020/11/5 0005 上午 11:02
**/
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
/**
* 过滤规则(注意优先级)
* —anon 无需认证(登录)可访问
* —authc 必须认证才可访问
* —perms[标识] 拥有资源权限才可访问
* —role 拥有角色权限才可访问
* —user 认证和自动登录可访问
*/
LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/login", "anon");
filterMap.put("/logout", "anon");
filterMap.put("/swagger**/**", "anon");
filterMap.put("/noAuth", "anon");
filterMap.put("/css/**", "anon");
filterMap.put("/js/**", "anon");
filterMap.put("/images/**", "anon");
filterMap.put("/lib/**", "anon");
filterMap.put("/favicon.ico", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);// 设置过滤规则
shiroFilterFactoryBean.setLoginUrl("/login");// 设置登录页面
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");// 未授权错误页面
return shiroFilterFactoryBean;
}
/**
* 自定义的Realm
*/
@Bean
public AuthRealm getRealm() {
return new AuthRealm();
}
/**
* 权限管理,配置主要是Realm的管理认证
* */
@Bean
public DefaultWebSecurityManager securityManager(AuthRealm authRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(authRealm);
return securityManager;
}
/**
* 启用shrio授权注解拦截方式,AOP式方法级权限检查
* 加入注解的使用,不加入这个注解不生效
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
- 启动项目,打开http://localhost:8080/swagger-ui.html调试接口
调试登录接口:/login
image.png
调试添加用户接口:/user/addUser
image.png
执行结果
image.png
调试编辑接口:/user/edit
image.png
换一个用户登录来调试编辑接口:/user/edit,会出现如下错误,因为不是admin角色
image.png
总结
好了,这里我们做了最简单的SpringBoot集成并使用Shiro安全框架,以后在权限系统里运用的比较多,最好结合项目使用。
网友评论