学习了一下Shrio,简单的做一下笔记
推荐三篇文章,有需要可以看看:
SpringBoot整合Shiro,通过用户、角色、权限三者关联实现权限管理
Shiro 关于
Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。相比较 Spring Security,Shiro 要小巧、简单的多。
一、准备
本文使用的 SpringBoot + MyBatis + thymeleaf
先看看项目结构
1.pom.xml
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
只贴出来关键依赖,其他可自行Maven
2.数据库 sql
-- 权限表--
CREATE TABLE permission(
pid INT(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL DEFAULT '',
url VARCHAR (255) DEFAULT '',
PRIMARY KEY (pid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
INSERT INTO permission VALUES ('1','add','');
INSERT INTO permission VALUES ('2','delete','');
INSERT INTO permission VALUES ('3','edit','');
INSERT INTO permission VALUES ('4','query','');
-- 用户表 --
CREATE TABLE user(
uid INT(11) NOT NULL AUTO_INCREMENT,
username VARCHAR(255) NOT NULL DEFAULT '',
password VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (uid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
INSERT INTO user VALUES ('1','admin','123');
INSERT INTO user VALUES ('2','demo','123');
-- 角色表 --
CREATE TABLE role(
rid INT(11) NOT NULL AUTO_INCREMENT,
rname VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (rid)
) ENGINE= InnoDB DEFAULT CHARSET = utf8;
INSERT INTO role VALUES ('1','admin');
INSERT INTO role VALUES ('2','customer');
-- 权限 角色 关系表 --
CREATE TABLE permission_role(
rid INT(11) NOT NULL,
pid INT(11) NOT NULL,
KEY idx_rid (rid),
KEY idx_pid(pid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
INSERT INTO permission_role VALUES ('1','1');
INSERT INTO permission_role VALUES ('1','2');
INSERT INTO permission_role VALUES ('1','3');
INSERT INTO permission_role VALUES ('1','4');
INSERT INTO permission_role VALUES ('2','1');
INSERT INTO permission_role VALUES ('2','4');
-- 用户 角色 关系表 --
CREATE TABLE user_role(
uid INT(11) NOT NULL,
rid INT(11) NOT NULL,
KEY idx_uid (uid),
KEY idx_rid (rid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
INSERT INTO user_role VALUES ('1','1');
INSERT INTO user_role VALUES ('2','2');
-- 为了验证加密效果 后面会被我用 ShiroMD5Util.MD5Pwd(),生成密码在 手动更改user表的密码 --
application.yml
DruidConfiguration文件就不写出来了
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
username: user
password: 123
# 数据源其他配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
#thymelea模板配置
thymeleaf:
cache: false
# mybatis
mybatis:
mapper-locations: mappers/*.xml
type-aliases-package: com.scitc.shiro.model
二、基础代码(Model,Dao和Service层)
1.Model 实体
User.java
package com.scitc.shiro.model;
import java.util.HashSet;
import java.util.Set;
public class User {
private Integer uid;
private String username;
private String password;
private Set<Role> roles = new HashSet<>();
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
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 Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "User{" +
"uid=" + uid +
", username='" + username + '\'' +
", password='" + password + '\'' +
", roles=" + roles +
'}';
}
}
Role.java
package com.scitc.shiro.model;
import java.util.HashSet;
import java.util.Set;
public class Role {
private Integer rid;
private String rname;
private Set<Permission> permissions = new HashSet<>();
private Set<User> users = new HashSet<>();
public Integer getRid() {
return rid;
}
public void setRid(Integer rid) {
this.rid = rid;
}
public String getRname() {
return rname;
}
public void setRname(String rname) {
this.rname = rname;
}
public Set<Permission> getPermissions() {
return permissions;
}
public void setPermissions(Set<Permission> permissions) {
this.permissions = permissions;
}
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
@Override
public String toString() {
return "Role{" +
"rid=" + rid +
", rname='" + rname + '\'' +
", permissions=" + permissions +
", users=" + users +
'}';
}
}
Permission.java
package com.scitc.shiro.model;
public class Permission {
private Integer pid;
private String name;
private String url;
public Integer getPid() {
return pid;
}
public void setPid(Integer pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public String toString() {
return "Permission{" +
"pid=" + pid +
", name='" + name + '\'' +
", url='" + url + '\'' +
'}';
}
}
2.mapper
UserMapper.java
public interface UserMapper {
User findByUsername(@Param("username") String username);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.scitc.shiro.mapper.UserMapper">
<resultMap id="userMap" type="com.scitc.shiro.model.User">
<id column="uid" property="uid"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<collection property="roles" ofType="com.scitc.shiro.model.Role">
<id column="rid" property="rid"/>
<result column="rname" property="rname"/>
<collection property="permissions" ofType="com.scitc.shiro.model.Permission">
<id column="pid" property="pid"/>
<result column="name" property="name"/>
<result column="url" property="url"/>
</collection>
</collection>
</resultMap>
<select id="findByUsername" parameterType="String" resultMap="userMap">
SELECT u.*,r.*,p.*
FROM user u
INNER JOIN user_role ur ON ur.uid = u.uid
INNER JOIN role r ON r.rid= ur.rid
INNER JOIN permission_role pr ON pr.rid = r.rid
INNER JOIN permission p ON pr.pid = p.pid
WHERE u.username = #{username}
</select>
</mapper>
//用户表--->用户角色表--->角色表--->角色权限表--->权限表
UserService
UserService.java
public interface UserService {
User findByUsername(String username);
}
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User findByUsername(String username) {
return userMapper.findByUsername(username);
}
}
三、整合Shiro
先定义一个 加密工具类 ShiroMD5Util.java
用户名一般是唯一的,所以盐值为:username + "salt"
若用户名后面可以修改,为保证两次(注册,登录)加密结果一致,可将 盐值添加到数据表的字段中
package com.scitc.shiro.utils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
public class ShiroMD5Util {
public static String MD5Pwd(String username, String pwd) {
//可用户注册、登录 时使用
// 加密算法MD5
// salt盐 username + salt
// 迭代次数 加密2次
String md5Pwd = new SimpleHash("MD5", pwd, ByteSource.Util.bytes(username + "salt"), 2).toHex();
return md5Pwd;
}
}
ShiroConfiguration.java
package com.scitc.shiro.config;
import com.scitc.shiro.CredentialMatcher;
import com.scitc.shiro.realm.AuthRealm;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
@Configuration
public class ShiroConfiguration {
//4.
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager manager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(manager);
bean.setLoginUrl("/login");
bean.setSuccessUrl("/index");
bean.setUnauthorizedUrl("/unauthorized");//访问 没有权限的页面时,将会被重定向到此链接,需在 Controller设置页面
// 1). anon 可以被 匿名访问
// 2). authc 必须认证(登录)才能访问
// 3). logout 登出
// 4). roles 角色过滤器
LinkedHashMap<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/index","authc");
filterChainDefinitionMap.put("/login","anon");
filterChainDefinitionMap.put("/loginuser","anon");
filterChainDefinitionMap.put("/admin","roles[admin]");
filterChainDefinitionMap.put("/edit","perms[edit]");//具有edit权限的才能访问
filterChainDefinitionMap.put("/druid/**","anon");//开放druid的监控后台
filterChainDefinitionMap.put("/**","user");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
//3.
// import org.apache.shiro.mgt.SecurityManager; 这个包
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(authRealm);
return manager;
}
//2
@Bean("authRealm")
public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher){
AuthRealm authRealm = new AuthRealm();
// 相关认证缓存到cache中
authRealm.setCacheManager(new MemoryConstrainedCacheManager());
authRealm.setCredentialsMatcher(matcher);
return authRealm;
}
// 1 密码采用加密方式进行验证:
// 本文使用的是自定义的校验规则,
// 另一种方式是 在bean 中 配置相关的加密算法
@Bean("credentialMatcher")
public CredentialMatcher credentialMatcher(){
return new CredentialMatcher();
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
//Shiro与Spring的关联 开启利用注解配置权限
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
*
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager")SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
AuthRealm.java
自定义的AuthRealm继承AuthorizingRealm。
并且重写父类中的doGetAuthorizationInfo(权限相关)、
doGetAuthenticationInfo(身份认证)这两个方法。
package com.scitc.shiro.realm;
import com.scitc.shiro.model.Permission;
import com.scitc.shiro.model.Role;
import com.scitc.shiro.model.User;
import com.scitc.shiro.service.UserService;
import org.apache.commons.collections.CollectionUtils;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class AuthRealm extends AuthorizingRealm {
private static final Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);
@Autowired
private UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
/**
* User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();
* User getAfterUser = (User)principalCollection.getPrimaryPrincipal();
* 两者效果一致,都是获取 user对象,
* 两者打印结果(登录用户为 admin):
* User{uid=1, username='admin', password='4b91fc877a3f4df7e812f98ebde4b5e5', roles=[Role{rid=1, rname='admin', permissions=[Permission{pid=1, name='add', url=''},
* Permission{pid=4, name='query', url=''}, Permission{pid=2, name='delete', url=''}, Permission{pid=3, name='edit', url=''}],
*/
//类似 session中获取用户
User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();
List<String> permissionList = new ArrayList<>();
List<String> roleNameList = new ArrayList<>();
//获取用户的角色,再获取角色拥有的所有权限
// 登录时数据库已经把所有的东西都获取出来了
Set<Role> roleSet = user.getRoles();
if (CollectionUtils.isNotEmpty(roleSet)) {
for (Role role : roleSet) {
roleNameList.add(role.getRname());
Set<Permission> permissionSet = role.getPermissions();
if (CollectionUtils.isNotEmpty(permissionSet)) {
for (Permission permission : permissionSet) {
permissionList.add(permission.getName());
}
}
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//所有操作权限
info.addStringPermissions(permissionList);//[delete, add, edit, query]
//授权时拿到了角色
info.addRoles(roleNameList);//[admin]
return info;
}
//认证登录 (执行完将会执行 CredentialMatcher.doCredentialsMatch()方法)
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
User user = userService.findByUsername(username);
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
}
}
CredentialMatcher.java
package com.scitc.shiro;
import com.scitc.shiro.utils.ShiroMD5Util;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
public class CredentialMatcher extends SimpleCredentialsMatcher {
//密码校验规则的重写
// AuthRealm 中 doGetAuthenticationInfo 执行之后会到达这里来 进行密码比对
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
/**
* 获取到用户输入的密码,进行盐值加密,加密方式与注册时相同,再与数据库中已加过密的密码进行比较
*/
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String password = new String(usernamePasswordToken.getPassword());
//把用户输入的密码 进行加密
password = ShiroMD5Util.MD5Pwd(((UsernamePasswordToken) token).getUsername(),password);
//数据库中的密码
String dbPassword = (String) info.getCredentials();
return this.equals(password, dbPassword);
}
}
TestController
四、验证登录
TestController.java
package com.scitc.shiro.controller;
import com.scitc.shiro.model.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
@Controller
public class TestController {
@RequestMapping("/login")
public String login() {
return "login";
}
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/unauthorized")
public String unauthorized() {
return "unauthorized";
}
@RequestMapping("/admin")
@ResponseBody
public String admin() {
return "Admin Success";
}
@RequestMapping("/edit")
@ResponseBody
public String edit() {
return "Edit Success";
}
//注解 测试
//用户必须拥有 delete 权限才能访问
@RequestMapping("/delete")
@RequiresPermissions("delete")
@ResponseBody
public String delete() {
return "我是test delete方法,测试注解是否有效";
}
@RequestMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
if (subject != null) {
subject.logout();
}
return "login";
}
@PostMapping("/loginuser")
public String loginUser(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpSession session) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
User user = (User) subject.getPrincipal();
session.setAttribute("user", user);
return "redirect:index";
} catch (Exception e) {
return "redirect:login";
}
}
}
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<h1>登录 </h1>
<form th:action="@{/loginuser}" method="post">
username:<input type="text" name="username"><br/>
password:<input type="password" name="password"><br/>
<input type="submit" value="提交">
</form>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en"xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1 >欢迎登录: [[${session.user.username}]]</h1>
<a th:href="@{/admin}">Admin Page</a><br/><br/>
<a th:href="@{/edit}">Edit Page</a><br/><br/>
<a th:href="@{/logout}">Logout</a>
</body>
</html>
unauthorized.html(没有权限就到此页面来 )
<!DOCTYPE html>
<html lang="en"xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>unauthorized</title>
</head>
<body>
<h1 >unauthorized</h1>
</body>
</html>
admin用户登录后访问,因为拥有所有权限, 所有页面不限制
访问 Admin Page
访问 Edit Page
访问 Delete Page
退出后: demo用户登录访问
访问 Admin Page
访问 Edit Page
访问 Delete Page
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。