Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。如同 Spring security 一样都是是一个权限安全框架,但是与Spring Security相比,在于他使用了和比较简洁易懂的认证和授权方式。
三大核心组件为:
-
Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
-
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器
-
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
在shiro的用户权限认证过程中其通过两个方法来实现:
- Authentication:是验证用户身份的过程。
- Authorization:是授权访问控制,用于对用户进行的操作进行人证授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
除了以上几个组件外,Shiro还有几个其他组件:
- SessionManager :Shiro为任何应用提供了一个会话编程范式。
- CacheManager :对Shiro的其他组件提供缓存支持。
整体结构图如下:
整合Shiro实现权限管理
配置maven如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.fly</groupId>
<artifactId>chat</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>chat</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<shiro.version>1.3.2</shiro.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
实现基本的权限管理需要用到5张表分别为:sys_user
(用户表),sys_menu
(菜单表),sys_role
(角色表),sys_role_menu
(角色菜单表),sys_user_role
(用户角色表)。首先创建数据库chat, 然后application.yml里数据库配置如下:
spring:
datasource:
url: jdbc:mysql://localhost:3306/chat?characterEncoding=utf-8&useSSL=true
driver-class-name: com.mysql.jdbc.Driver
username: tele
password: 123456
thymeleaf:
encoding: UTF-8
cache: false
mode: HTML5
jpa:
show-sql: true
hibernate:
ddl-auto: update
创建三个实体类。
Menu:
@Entity
@Table(name = "sys_menu")
public class Menu implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 名称
*/
private String name;
/**
* 链接
*/
private String href;
/**
* 父菜单id
*/
private String parentId;
/**
* 权限标识
*/
private String permission;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getHref() {
return href;
}
public void setHref(String href) {
this.href = href;
}
public String getPermission() {
return permission;
}
public void setPermission(String permission) {
this.permission = permission;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
}
Role:
@Entity
@Table(name = "sys_role")
public class Role implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 角色名
*/
private String name;
/**
* 角色所对应的能访问的菜单
*/
@ManyToMany(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
@JoinTable(name = "sys_role_menu", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "menu_id", referencedColumnName = "id"))
private List<Menu> menuList = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Menu> getMenuList() {
return menuList;
}
public void setMenuList(List<Menu> menuList) {
this.menuList = menuList;
}
}
User:
@Entity
@Table(name = "sys_user")
public class User implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 用户所对应的角色
*/
@ManyToMany(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
@JoinTable(name = "sys_user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> roleList = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public List<Role> getRoleList() {
return roleList;
}
public void setRoleList(List<Role> roleList) {
this.roleList = roleList;
}
}
创建之后因为jpa.hibernate.ddl-auto设置为了update,运行项目之后将会在数据里自动创建5张表,如下图:
配置Shiro:
@Configuration
public class ShiroConfig {
private Logger logger = Logger.getLogger(this.getClass());
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
*
* Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//获取filters
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
//将自定义 的FormAuthenticationFilter注入shiroFilter中
filters.put("authc", new FormAuthenticationFilter());
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.html"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/");
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
/**
* 配置shiro拦截器链
*
* anon 不需要认证
* authc 需要认证
* user 验证通过或RememberMe登录的都可以
*
*/
//配置记住我或认证通过可以访问的地址
filterChainDefinitionMap.put("/", "user");
filterChainDefinitionMap.put("/login", "authc");
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
//所有url都必须有user权限才可以访问 一般讲/**放在最后面
filterChainDefinitionMap.put("/**", "user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
logger.info("================Shiro拦截器工厂类注入成功================");
return shiroFilterFactoryBean;
}
//配置核心安全事务管理器
@Bean(name = "securityManager")
public SecurityManager securityManager(@Qualifier("systemAuthorizingRealm") SystemAuthorizingRealm systemAuthorizingRealm) {
logger.info("===========shrio已加载===================");
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(systemAuthorizingRealm);
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* 身份认证realm; (这个需要自己写,账号密码校验;权限等)
*
* @return
*/
@Bean(name = "systemAuthorizingRealm")
public SystemAuthorizingRealm systemAuthorizingRealm() {
return new SystemAuthorizingRealm();
}
/**
* thymleaf里使用shiro
* @return
*/
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect() {
logger.info("======shiroDialect========================");
return new ShiroDialect();
}
/**
* 记住我Cookie
* @return
*/
@Bean
public SimpleCookie rememberMeCookie(){
logger.info("======ShiroConfiguration.rememberMeCookie()========================");
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//<!-- 记住我cookie生效时间30天 ,单位秒;-->
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
/**
* cookie管理对象;
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
return cookieRememberMeManager;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator=new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager manager) {
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(manager);
return advisor;
}
}
自定义FormAuthenticationFilter,SystemAuthorizingRealm,UsernamePasswordToken。
1. FormAuthenticationFilter:
public class FormAuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter{
public static final String DEFAULT_MESSAGE_PARAM = "message";
private String messageParam = DEFAULT_MESSAGE_PARAM;
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String username = getUsername(request);
String password = getPassword(request);
if (password==null){
password = "";
}
boolean rememberMe = isRememberMe(request);
String host = StringUtils.getRemoteAddr((HttpServletRequest)request);
return new UsernamePasswordToken(username, password.toCharArray(), rememberMe, host);
}
/**
* 获取登录用户名
*/
@Override
protected String getUsername(ServletRequest request) {
String username = super.getUsername(request);
if (StringUtils.isBlank(username)){
username = StringUtils.toString(request.getAttribute(getUsernameParam()), StringUtils.EMPTY);
}
return username;
}
/**
* 获取登录密码
*/
@Override
protected String getPassword(ServletRequest request) {
String password = super.getPassword(request);
if (StringUtils.isBlank(password)){
password = StringUtils.toString(request.getAttribute(getPasswordParam()), StringUtils.EMPTY);
}
return password;
}
/**
* 获取记住我
*/
@Override
protected boolean isRememberMe(ServletRequest request) {
String isRememberMe = WebUtils.getCleanParam(request, getRememberMeParam());
if (StringUtils.isBlank(isRememberMe)){
isRememberMe = StringUtils.toString(request.getAttribute(getRememberMeParam()), StringUtils.EMPTY);
}
return StringUtils.toBoolean(isRememberMe);
}
public String getMessageParam() {
return messageParam;
}
/**
* 登录成功之后跳转URL
*/
@Override
public String getSuccessUrl() {
return super.getSuccessUrl();
}
@Override
protected void issueSuccessRedirect(ServletRequest request,
ServletResponse response) throws Exception {
WebUtils.issueRedirect(request, response, getSuccessUrl(), null, true);
}
/**
* 登录失败调用事件
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token,
AuthenticationException e, ServletRequest request, ServletResponse response) {
String className = e.getClass().getName(), message = "";
if (IncorrectCredentialsException.class.getName().equals(className)
|| UnknownAccountException.class.getName().equals(className)){
message = "用户或密码错误, 请重试.";
}
else if (e.getMessage() != null && StringUtils.startsWith(e.getMessage(), "msg:")){
message = StringUtils.replace(e.getMessage(), "msg:", "");
}
else{
message = "系统出现点问题,请稍后再试!";
e.printStackTrace(); // 输出到控制台
}
request.setAttribute(getFailureKeyAttribute(), className);
request.setAttribute(getMessageParam(), message);
return true;
}
}
2. SystemAuthorizingRealm:
public class SystemAuthorizingRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 认证回调函数, 登录时调用 设置了authc才会进入
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 校验用户名密码
User user = userService.findUserByUserName(token.getUsername());
if (user != null) {
//如果身份认证验证成功,返回一个AuthenticationInfo实现;
return new SimpleAuthenticationInfo(new Principal(user),user.getPassword(),getName());
}else {
return null;
}
}
/**
* 获取权限授权信息,如果缓存中存在,则直接从缓存中获取,否则就重新获取, 登录成功后调用
*/
@Override
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
return null;
}
//实际项目中这里可以设置缓存,从缓存中读取
return doGetAuthorizationInfo(principals);
}
/**
* 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Principal principal = (Principal) getAvailablePrincipal(principals);
User user = userService.findUserByUserName(principal.getName());
if (user != null) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Menu> list = userService.getMenuList();
for (Menu menu : list){
if (StringUtils.isNotBlank(menu.getPermission())){
// 添加基于Permission的权限信息
for (String permission : StringUtils.split(menu.getPermission(),",")){
info.addStringPermission(permission);
}
}
}
// 添加用户权限
info.addStringPermission("user");
return info;
} else {
return null;
}
}
@Override
protected void checkPermission(Permission permission, AuthorizationInfo info) {
authorizationValidate(permission);
super.checkPermission(permission, info);
}
@Override
protected boolean[] isPermitted(List<Permission> permissions, AuthorizationInfo info) {
if (permissions != null && !permissions.isEmpty()) {
for (Permission permission : permissions) {
authorizationValidate(permission);
}
}
return super.isPermitted(permissions, info);
}
@Override
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
authorizationValidate(permission);
return super.isPermitted(principals, permission);
}
@Override
protected boolean isPermittedAll(Collection<Permission> permissions, AuthorizationInfo info) {
if (permissions != null && !permissions.isEmpty()) {
for (Permission permission : permissions) {
authorizationValidate(permission);
}
}
return super.isPermittedAll(permissions, info);
}
/**
* 授权验证方法
* @param permission
*/
private void authorizationValidate(Permission permission){
// 模块授权预留接口
}
/**
* 授权用户信息
*/
public static class Principal implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
public Principal(User user) {
this.id = user.getId();
this.username = user.getUsername();
}
public Long getId() {
return id;
}
public String getName() {
return username;
}
@Override
public String toString() {
return "Principal{" +
"id=" + id +
'}';
}
}
}
3. UsernamePasswordToken:
public class UsernamePasswordToken extends org.apache.shiro.authc.UsernamePasswordToken {
private static final long serialVersionUID = 1L;
public UsernamePasswordToken() {
super();
}
public UsernamePasswordToken(String username, char[] password,
boolean rememberMe, String host) {
super(username, password, rememberMe, host);
}
}
LoginController:
@Controller
public class LoginController {
private Logger logger = Logger.getLogger(this.getClass());
/**
* 进入登录界面
* @return
*/
@GetMapping("/login")
public String login(){
return "login";
}
/**
* 登录成功,进入管理界面 需要用户权限
* @return
*/
@RequiresPermissions("user")
@GetMapping("/")
public ModelAndView index(Model model){
//根菜单
// List<Menu> menus = systemService.findMenusByParentId(rootId);
// model.addAttribute("list",menus);
return new ModelAndView("index","menuModel",model);
}
/**
* 登录失败,真正登录的POST请求由Filter完成
* @param request
* @return
*/
@PostMapping("login")
public String login(HttpServletRequest request , User user , Model model){
logger.info("username:" + user.getUsername());
SystemAuthorizingRealm.Principal principal = UserUtils.getPrincipal();
// 如果已经登录,则跳转到管理首页
if(principal != null){
return "redirect:" + "/";
}
String username = WebUtils.getCleanParam(request, FormAuthenticationFilter.DEFAULT_USERNAME_PARAM);
String message = (String)request.getAttribute(FormAuthenticationFilter.DEFAULT_MESSAGE_PARAM);
if (StringUtils.isBlank(message) || StringUtils.equals(message, null)){
message = "用户或密码错误, 请重试.";
}
model.addAttribute(FormAuthenticationFilter.DEFAULT_USERNAME_PARAM, username);
model.addAttribute(FormAuthenticationFilter.DEFAULT_MESSAGE_PARAM, message);
model.addAttribute("loginError", true);
return "login";
}
}
login.html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content=""/>
<meta name="author" content=""/>
<title>系统登录</title>
<!-- Bootstrap core CSS -->
<!-- Bootstrap core CSS -->
<link href="../../css/bootstrap/bootstrap.min.css" th:href="@{/css/bootstrap/bootstrap.min.css}" rel="stylesheet"/>
<link href="../../css/signin.css" th:href="@{/css/signin.css}" rel="stylesheet"/>
</head>
<body class="login_bg">
<div class="container">
<div class="signin-div">
<form class="form-signin" th:action="@{/login}" method="post">
<input type="text" id="username" name="username" class="f_control f_control_f" placeholder="用户名"/>
<!-- <label for="password" class="sr-only">密 码:</label> -->
<input type="password" id="password" name="password" class="f_control" placeholder="密码"/>
<div class="checkbox">
<label>
<input type="checkbox" id="rememberMe" name="rememberMe"/> 记住密码
</label>
</div>
<div class=" col-md-10" th:if="${loginError}">
<p class="login-error" th:text="${message}"></p>
</div>
<button class="btn btn-lg btn-primary btn-block submit" type="submit">
登 录
</button>
</form>
</div>
</div> <!-- /container -->
<script src="../../js/jquery-3.1.1.min.js" th:src="@{/js/jquery-3.1.1.min.js}"></script>
</body>
</html>
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
index
</body>
</html>
这里用到了thymeleaf模板,bootstrap,首先在表sys_user中插入一条数据用户名为admin,密码为123456。登录时具体的流程为点击登录首先进入FormAuthenticationFilter类中的createToken,然后返回自定义的UsernamePasswordToken,接着进入认证回掉函数SystemAuthorizingRealm类中的doGetAuthenticationInfo,在这里认证输入的用户名和密码是否正确,验证失败会进入FormAuthenticationFilter中的onLoginFailure,然后进入LoginController中的login处理失败信息,认证成功之后进入LoginController里的index,然后这里设置了@RequiresPermissions("user"),需要有用户权限才能访问,然后就会进入SystemAuthorizingRealm里的getAuthorizationInfo去获取授权信息,这里就可以给用户添加相关的菜单权限了,大概的认证和授权流程就是这样的。html里面可以使用shiro:hasAllPermissions="sys-user-edit"来验证权限,有权限就会显示,无权限将不会显示,这里还有一些前端界面代码没有贴出来,如用户管理,权限管理等,由于需要用到一些js插件,篇幅限制没有贴出来,大家可以下载源码。
IDEA热部署
1. pom文件中加入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
2. 设置IDEA自动编译:
按快捷键Crtl+Shift+Alt+/ :
点击Registry:
讲app.running勾上。