1. 权限管理
1.1 什么是权限管理
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或安全策略
控制用户可以访问并且只能访问被授权的资源。
1.2 什么是身份认证
身份认证
就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。
1.3 什么是身份授权
身份授权
即访问控制,控制谁能访问那些资源,不同的用户应该拥有不同的资源访问权限,常见的有学校的教务管理系统:有教师,学生,管理员登录几个模块,以不同的身份登录就会显示不同的界面!
2. 什么是shiro
shiro是Apache旗下的一个开源框架,它将软件系统的安全认证相关的功能抽取出来,是一个功能强大且简单易用的Java安全框架,它可以用来进行身份验证,授权,加密和会话管理。
3. shiro核心架构
3.1 Subject
Subject即主体
,外部应用与Subject进行交互,Subject对象记录了当前操作的用户,将用户的概念理解成当前操作的主体,可能是一个浏览器的请求的用户,也可能是一个允许的程序。Subject在shiro中是一个接口,接口中定义了很多认证授权的方法,外部程序通过Subject对象进行认证和授权,而Subject对象是通过SecurityManager进行认证和授权!
3.2 SecurityManager
SecurityManager即安全管理器
,对全部的Subject对象进行安全管理,它是shiro的核心,通过SecurityManager可以完成对Subject对象的认证授权等系列操作,实质上是使用Authenticator进行认证,使用Authorizer进行授权,使用SessionManager进行会话管理!
3.3 Authenticator
Authenticator即认证器
,对用户身份进行认证,Authenticator是一个接口,shiro提供了ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足认证的大部分需求,也可以使用自定义的认证器。
3.4 Authorizer
Authorizer即授权器
,用户通过认证器认证通过,在访问功能时需要通过授权器
判断用户是否由此功能的操作权限。
3.5 Realm
Realm即领域
,相当于datasource,SecurityManager进行安全认证是需要从Realm获取用户权限数据,比如:用户的身份信息如果放到数据库中,那么就需要从数据库中获取到用户身份信息。
注:不要把Realm认为只是在里面取数据,在Realm还有认证和授权相关的代码!
3.6 SessionManager
SessionManager即会话管理
,shiro框架定义了一系列会话管理,它不依赖于web的session,所以shiro可以使用在非web的环境下,也可以将分布式的应用集中在一起管理,此特性可以实现单点登录!
3.7 SessionDao
SessionDao即会话dao
,是对session会话操作的一套接口,比如将session存储到数据库,也可以使用jdbc将session存入数据库。
3.8 CacheManager
CacheManager即缓存管理器
,将用户权限数据存储到缓存中,可以提高性能。
3.9 Cryptography
Cryptography即密码管理
,shiro提供了一套加密/解密的组件,方便开发,比如提供了常用分散列,盐值计算,加/解密等功能!
4. shiro的认证
4.1 认证
认证就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。
4.2 认证的关键对象
- Subject:主体
访问系统的用户,主体可以是程序,用户等,进行认证的都称为主体。
- Principal:身份信息
是主体进行身份认证的标识,标识必须具有唯一性,如电话号码,手机号,邮箱地址,一个主体可以有多个身份,但必须有一个主身份!
- credential:凭证信息
只有主体自己知道的安全信息,如密码,证书等。
4.3 认证流程
4.4 认证的开发
使用本地shiro.ini文件开发
- 创建maven项目引入依赖
<!--引入shiro相关依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
- 引入shiro.ini配置文件
- 开发认证代码
/** * 测试认证(加载本地的realm) * */
public class TestAuthenticator
{
public static void main( String[] args )
{
//1.创建SecurityManager对象
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini")); //可以从数据库中获取realm
//2.将安装工具类设置为默认的安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//3.获取主体对象
Subject subject = SecurityUtils.getSubject();
//4.创建token令牌
UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu","809080");
//5.执行登录方法
try {
subject.login(token);
System.out.println("登陆成功!!");
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误!!");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误!!");
}
}
}
4.5 自定义Realm的开发
使用本地shiro.ini开发,使用的realm就是我们本地提供的,而实际开发中,realm的数据往往是我们的数据库中读取!
-
shiro提供的Realm
-
Realm的实现类中认证是使用SimpleAccountRealm
SimpleAccountRealm 部分源码中有两个方法,分别是认证:doGetAuthenticationInfo(AuthenticationToken token)
,授权:doGetAuthorizationInfo(PrincipalCollection principals)
//判断用户是否为合法用户
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken)token;
SimpleAccount account = this.getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
//为合法用户授权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = this.getUsername(principals);
this.USERS_LOCK.readLock().lock();
AuthorizationInfo var3;
try {
var3 = (AuthorizationInfo)this.users.get(username);
} finally {
this.USERS_LOCK.readLock().unlock();
}
return var3;
}
- 自定义Realm
/** * 自定义realm,分别实现认证和授权方法 */
public class CustomerRealm extends AuthorizingRealm {
//身份认证
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//授权
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取用户名key
String principal = (String) authenticationToken.getPrincipal();
//根据前台传递过来的principal身份信息去使用jdbc或者mybatis查询数据库
if("liuzeyu".equals(principal)){
/** * principal:返回当前数据库中的正确用户名 * credentials:返回当前数据库中正确的密码 * realmName:当前realm名字 */
return new SimpleAuthenticationInfo(principal,"809080",this.getName());
}
return null;
}
}
- 使用自定义Realm进行认证
/** * 测试加载远程数据库的realm */
public class TestCustomerAuthenticator {
public static void main(String[] args) {
//1.创建SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(new CustomerRealm());
//2.将安装工具设置为默认的安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//3.获取主体对象
Subject subject = SecurityUtils.getSubject();
//4.获取令牌
UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu", "809080");
//5.执行
try {
subject.login(token);
System.out.println("登陆成功!!");
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
}
}
4.6 对密码进行加盐和hash
- 测试加密
/** * 测试hash,salt,散列 */
public class TestHash {
public static void main(String[] args) {
//1.不加salt和散列次数
Md5Hash md5Hash1 = new Md5Hash("809080");
System.out.println(md5Hash1.toHex()); //转成16进制字符串
//2.加salt,不添加散列次数
Md5Hash md5Hash2 = new Md5Hash("809080","Xq*07");
System.out.println(md5Hash2.toHex());
//3.加salt,添加散列次数1024次
Md5Hash md5Hash3 = new Md5Hash("809080","Xq*07",1024);
System.out.println(md5Hash3.toHex());
}
}
- 为明文加入加密算法
public class CustomerReleamMd5 extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
//利用假数据去匹配
if("liuzeyu".equals(principal)){
return new SimpleAuthenticationInfo(principal,
"641acb1f58ed7e24677a1e26d2497604", //传递不同的密码强度验证
ByteSource.Util.bytes("Xq*07"),
this.getName());
}
return null;
}
}
public class TestCustomerMd5Authenticator {
public static void main(String[] args) {
//身份验证
//1.1创建安全管理器
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置自定义realm获取认证数据
CustomerReleamMd5 releamMd5 = new CustomerReleamMd5();
//创建凭证匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5"); //(md5)算法
hashedCredentialsMatcher.setHashIterations(1024); //散列1024次,默认是一次
releamMd5.setCredentialsMatcher(hashedCredentialsMatcher); //为凭证设置凭证适配器(hash)
defaultSecurityManager.setRealm(releamMd5);
//1.2设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//1.3获取主体
Subject subject = SecurityUtils.getSubject();
//1.4获取令牌
UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu","809080");
//1.5执行
try {
subject.login(token);
System.out.println("登陆成功");
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
}
}
5. shiro的授权
在自定义的Realm基础上加入授权功能,只有通过认证用户才可以被授权!
- 自定义Realm
public class CustomerReleamMd5 extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
System.out.println(primaryPrincipal);
System.out.println("primaryPrincipal:"+primaryPrincipal);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin"); //添加角色admin
simpleAuthorizationInfo.addStringPermission("product:*:01"); //权限字符串 product 可以对01实例执行任何操作
return simpleAuthorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
//利用假数据去匹配
if("liuzeyu".equals(principal)){
return new SimpleAuthenticationInfo(principal,
"641acb1f58ed7e24677a1e26d2497604", //传递不同的密码强度验证
ByteSource.Util.bytes("Xq*07"),
this.getName());
}
return null;
}
}
- 测试
public class TestCustomerMd5Authenticator {
public static void main(String[] args) {
//身份验证
//1.1创建安全管理器
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置自定义realm获取认证数据
CustomerReleamMd5 releamMd5 = new CustomerReleamMd5();
//创建凭证匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5"); //(md5)算法
hashedCredentialsMatcher.setHashIterations(1024); //散列1024次,默认是一次
releamMd5.setCredentialsMatcher(hashedCredentialsMatcher); //为凭证设置凭证适配器(hash)
defaultSecurityManager.setRealm(releamMd5);
//1.2设置安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//1.3获取主体
Subject subject = SecurityUtils.getSubject();
//1.4获取令牌
UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu","809080");
//1.5执行
try {
subject.login(token);
System.out.println("登陆成功");
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
//2.授权
//如果认证通过
if(subject.isAuthenticated()){
//基于角色管理
boolean admin = subject.hasRole("admin");
System.out.println(admin);
//基于资源管理
boolean permitted = subject.isPermitted("product:create:01");
System.out.println(permitted);
}
}
}
6. spring boot整合shiro
模板引擎使用原生的JSP,配合ShiroFilter实现用户的认证和授权,实现用户注册,登录,可以以不同的身份显示不同的资源!
1. spring boot前期准备
- 导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- springboot启动shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
<!-- shiro默认缓存-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
<!--redis整合springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入springboot解析jsp依赖-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- 引入mybatis相关的启动依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- mysql相关依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!-- druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
</dependencies>
- 准备SQL文件
/* Navicat Premium Data Transfer Source Server : xiaochen的数据库 Source Server Type : MySQL Source Server Version : 50718 Source Host : 127.0.0.1:3306 Source Schema : shiro Target Server Type : MySQL Target Server Version : 50718 File Encoding : 65001 Date: 29/05/2020 16:31:22 */
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_perms`;
CREATE TABLE `t_perms` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`name` varchar(80) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_perms
-- ----------------------------
BEGIN;
INSERT INTO `t_perms` VALUES (1, 'user:*:*', '');
INSERT INTO `t_perms` VALUES (2, 'product:*:01', NULL);
INSERT INTO `t_perms` VALUES (3, 'order:*:*', NULL);
COMMIT;
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`name` varchar(60) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_role
-- ----------------------------
BEGIN;
INSERT INTO `t_role` VALUES (1, 'admin');
INSERT INTO `t_role` VALUES (2, 'user');
INSERT INTO `t_role` VALUES (3, 'product');
COMMIT;
-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
`id` int(6) NOT NULL,
`roleid` int(6) DEFAULT NULL,
`permsid` int(6) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_role_perms
-- ----------------------------
BEGIN;
INSERT INTO `t_role_perms` VALUES (1, 1, 1);
INSERT INTO `t_role_perms` VALUES (2, 1, 2);
INSERT INTO `t_role_perms` VALUES (3, 2, 1);
INSERT INTO `t_role_perms` VALUES (4, 3, 2);
INSERT INTO `t_role_perms` VALUES (5, 1, 3);
COMMIT;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`username` varchar(40) DEFAULT NULL,
`password` varchar(40) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_user
-- ----------------------------
BEGIN;
INSERT INTO `t_user` VALUES (1, 'xiaochen', '24dce55acdcb3b6363c7eacd24e98cb7', '28qr0xu%');
INSERT INTO `t_user` VALUES (2, 'zhangsan', 'ca9f1c951ce2bfb5669f3723780487ff', 'IWd1)#or');
COMMIT;
-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`id` int(6) NOT NULL,
`userid` int(6) DEFAULT NULL,
`roleid` int(6) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_user_role
-- ----------------------------
BEGIN;
INSERT INTO `t_user_role` VALUES (1, 1, 1);
INSERT INTO `t_user_role` VALUES (2, 2, 2);
INSERT INTO `t_user_role` VALUES (3, 2, 3);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
- application.properties配置文件
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
spring.application.name=shiro
server.servlet.context-path=/shiro
server.port=8888
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=809080
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?useUnicode=true&useSSL=false&characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
mybatis.type-aliases-package=com.liuzeyu.entity
mybatis.mapper-locations=classpath:com/liuzeyu/mapper/*.xml
logging.level.com.liuzeyu.mapper=debug
#相关配置
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0
- mapper层开发
@Mapper
public interface UserMapper {
public void save(User user);
public User findByUsername(String username);
public User findRolesByUsername(String username);
public List<Perms> findPermsByRoleId(Integer id);
}
表和表之间的关系:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liuzeyu.mapper.UserMapper">
<insert id="save" useGeneratedKeys="true" parameterType="user" keyProperty="id" >
insert into t_user values(#{id},#{username},#{password},#{salt})
</insert>
<select id="findByUsername" parameterType="String" resultType="user">
select * from t_user where username=#{username}
</select>
<!--查询的是一个用户对应的多个角色信息-->
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<collection property="roles" javaType="list" ofType="Role">
<id column="rid" property="id"/>
<result column="name" property="name"/>
</collection>
</resultMap>
<select id="findRolesByUsername" parameterType="String" resultMap="userMap">
SELECT
u.id,u.username,r.id rid,r.name
from
t_user u
left join
t_user_role ur
on
u.id = ur.userid
left join
t_role r
on
r.id = ur.roleid
where
u.username = #{username}
</select>
<select id="findPermsByRoleId" parameterType="Integer" resultType="Perms">
select p.id,p.name,r.name
from
t_role r
left join
t_role_perms rp
on
rp.roleid=r.id
left join
t_perms p
on
p.id = rp.permsid
where r.id=#{id}
</select>
</mapper>
- service层开发
public interface UserService {
public void save(User user);
public User findByUsername(String username);
public User findRolesByUsername(String username);
public List<Perms> findPermsByRoleId(Integer id);
}
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper mapper;
@Override
public void save(User user) {
//获取盐
String salt = SaltUtils.getSalt(8);
user.setSalt(salt);
Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
user.setPassword(md5Hash.toHex());
mapper.save(user);
}
@Override
public User findByUsername(String username) {
return mapper.findByUsername(username);
}
@Override
public User findRolesByUsername(String username) {
return mapper.findRolesByUsername(username);
}
@Override
public List<Perms> findPermsByRoleId(Integer id) {
return mapper.findPermsByRoleId(id);
}
}
2. shrio框架的整合!
- ShiroConfig配置
@Configuration
public class ShiroConfig {
//1.创建ShiroFilter
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
//创建Shiro的filter
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//注入安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统受限资源
Map<String,String> map = new HashMap<String,String>();
map.put("/user/login", "anon"); //将不需要授权的资源放最上面
map.put("/user/register", "anon");
map.put("/register.jsp","anon");
map.put("/**", "authc"); //authc请求这个资源需要认证和授权。/**表示对所有的资源认证
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//shiroFilterFactoryBean.setLoginUrl("/login.jsp"); 默认请求受限资源跳转login.jsp
//配置系统公共资源
return shiroFilterFactoryBean;
}
//2.创建web安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("realm") Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
@Bean("realm")
//创建自定义realm
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//配置凭证适配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
//开启shiro默认的ehcache缓存机制
customerRealm.setCachingEnabled(true); //启动缓存
customerRealm.setAuthorizationCachingEnabled(true); //启动认证缓存
customerRealm.setAuthorizationCacheName("authorizationCache"); //命名
customerRealm.setAuthenticationCachingEnabled(true); //启动授权缓存
customerRealm.setAuthenticationCacheName("authenticatonCache"); //命名
customerRealm.setCacheManager(new RedisCacheManager()); //使用Redis提供的缓存机制
return customerRealm;
}
}
- Realm开发
public class CustomerRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
//假数据
// if("liuzeyu12a".equals(primaryPrincipal)){
// SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// simpleAuthorizationInfo.addRole("user");
// simpleAuthorizationInfo.addStringPermission("user:create:*");
// simpleAuthorizationInfo.addStringPermission("user:save:*");
// }
User user = userService.findRolesByUsername(primaryPrincipal);
if(!ObjectUtils.isEmpty(user.getRoles())){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role->{
simpleAuthorizationInfo.addRole(role.getName());
List<Perms> perms = userService.findPermsByRoleId(role.getId());
if(!CollectionUtils.isEmpty(perms)){
perms.forEach(perm->{
simpleAuthorizationInfo.addStringPermission(perm.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
//连接数据库做查询用户名是否存在
//假数据 if("liuzeyu12a".equals(principal)){
// return new SimpleAuthenticationInfo(principal, "809080", this.getName());
// }
User user = userService.findByUsername(principal);
if(!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(principal,
user.getPassword(),
new MyByteSource(user.getSalt()),
this.getName());
}
return null;
}
}
- Redis缓存实现了Shiro提供的CacheManager
public class RedisCacheManager implements CacheManager {
// 参数:认证或者授权的名字
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
System.out.println("缓存名称:"+cacheName);
return new RedisCache<K,V>(cacheName);
}
}
- Reids缓冲器
public class RedisCache<K,V> implements Cache<K,V> {
private String cacheName;
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
public RedisCache() {
}
@Override
public V get(K k) throws CacheException {
System.out.println("获取缓存:"+k);
return (V) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
System.out.println("设置缓存:"+"k:"+k+" value:"+v);
getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
return null;
}
@Override
public V remove(K k) throws CacheException {
return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
}
@Override
public void clear() throws CacheException {
getRedisTemplate().delete(this.cacheName);
}
@Override
public int size() {
return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<K> keys() {
return getRedisTemplate().opsForHash().keys(this.cacheName);
}
@Override
public Collection<V> values() {
return getRedisTemplate().opsForHash().values(this.cacheName);
}
//封装获取redisTempalte
private RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate = (RedisTemplate)ApplicationContextUtils.getBean("redisTemplate");
//设置序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
3. 工具类和实体类
- ApplicationContextUtils:
/** * 功能就是从IOC容器中获取bean */
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
//根据bean名称从工厂中获取bean对象
public static Object getBean(String name){
return applicationContext.getBean(name);
}
}
- 自定义salt实现,实现序列化接口
//自定义salt实现,实现序列化接口
public class MyByteSource extends SimpleByteSource implements Serializable {
public MyByteSource(String string) {
super(string);
}
}
- 获取随机盐
/** * 获取随机盐 */
public class SaltUtils {
public static String getSalt(int n){
StringBuilder sb = new StringBuilder();
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"abcdefghijklmnopqrstuvwxyz!@#$%^&*()";
char[] code = chars.toCharArray();
for (int i = 0; i < n; i++) {
sb.append(code[new Random().nextInt(code.length)]);
}
return sb.toString();
}
}
- 实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class User implements Serializable {
private Integer id;
private String username;
private String password;
private String salt;
private List<Role> roles;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Role implements Serializable {
private Integer id;
private String name;
private List<Perms> perms;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Perms implements Serializable {
private Integer id;
private String name;
private String url;
}
4. web层开发
- controller层开发
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService service;
/** * 处理身份验证 * @param username * @param password * @return */
@PostMapping("/login")
public String login(String username,String password){
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username,password));
System.out.println("登陆成功");
return "redirect:/index.jsp";
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名不存在");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
return "redirect:/login.jsp";
}
/** * 用户退出登录 * @return */
@GetMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login.jsp";
}
@PostMapping("/register")
public String register(User user){
try {
service.save(user);
System.out.println("注册成功");
return "redirect:/login.jsp";
}catch (Exception e){
e.printStackTrace();
System.out.println("注册失败");
return "redirect:/register.jsp";
}
}
}
- JSP界面
login.jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>登录页面</h2>
<form action="${pageContext.request.contextPath}/user/login" method="post">
账号:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="登录"><br>
</form>
</body>
</html>
register.jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>注册页面</h2>
<form action="${pageContext.request.contextPath}/user/register" method="post">
账号:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="立即注册"><br>
</form>
</body>
</html>
index.jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" language="java" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统主页</title>
</head>
<body>
<h1>V0.1 系统主页</h1>
<ul>
<shiro:hasAnyRoles name="admin,user">
<li><a href="">用户管理</a>
<ul>
<ui>
<shiro:hasPermission name="user:create:*">
<li>创建</li>
</shiro:hasPermission>
<shiro:hasPermission name="user:delete:01">
<li>删处</li>
</shiro:hasPermission>
<shiro:hasPermission name="user:update:01">
<li>修改</li>
</shiro:hasPermission>
<shiro:hasPermission name="user:select:01">
<li>查询</li>
</shiro:hasPermission>
</ui>
</ul>
</li>
</shiro:hasAnyRoles>
<shiro:hasAnyRoles name="admin,product,order">
<shiro:hasPermission name="product:*:01">
<li><a href="">商品管理</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="order:*:01">
<li><a href="">订单管理</a></li>
</shiro:hasPermission>
<li><a href="">物流管理</a></li>
</shiro:hasAnyRoles>
</ul>
<a href="${pageContext.request.contextPath}/user/logout">退出</a>
</body>
</html>
5. 集成验证码
- 修改登录页面
请输入验证码:<input type="text" name="code"/><img src="${pageContext.request.contextPath}/user/getImage">
- controller层开发
@PostMapping("/login")
public String login(String username,String password,String code,HttpSession session){
String codes = (String) session.getAttribute("code");
try {
if(codes.equalsIgnoreCase(code)){
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username,password));
System.out.println("登陆成功");
return "redirect:/index.jsp";
}else{
throw new RuntimeException("验证码错误");
}
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名不存在");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}catch (RuntimeException e){
e.printStackTrace();
System.out.println(e.getMessage());
}
return "redirect:/login.jsp";
}
@RequestMapping("/getImage")
public void getImage(HttpSession session, HttpServletResponse response){
try {
String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
session.setAttribute("code",verifyCode);
ServletOutputStream os = response.getOutputStream();
//验证码存入图片
response.setContentType("image/png");
VerifyCodeUtils.outputImage(220, 60, os , verifyCode);
} catch (IOException e) {
e.printStackTrace();
}
}
- 需要重新修改salt的序列化规则
改用实现ByteSource 接口,因为SimpleByteSource 没有提供默认的无参构造,序列化会出现问题!
//自定义salt实现,实现序列化接口
public class MyByteSource implements ByteSource , Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public MyByteSource(){
}
public MyByteSource(byte[] bytes) {
this.bytes = bytes;
}
public MyByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public MyByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public MyByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public MyByteSource(File file) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
}
public MyByteSource(InputStream stream) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public byte[] getBytes() {
return this.bytes;
}
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
public String toString() {
return this.toBase64();
}
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource)o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
7. spring boot整合shiro之thymeleaf权限控制
在springboot整合Shiro的基础上,修改模板引擎为thymeleaf,需要注意的是thymeleaf界面的跳转要借助于控制器!
- 修改application.properties
将
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
修改为
spring.thymeleaf.cache=false
spring.thymeleaf.suffix=.html
spring.thymeleaf.prefix=classpath:/templates/
- 添加springboot的thymeleaf的启动依赖和shiro整合thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
- 修改JSP页面为html页面
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>登录页面</h2>
<form th:action="@{/user/login}" method="post">
账号:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
请输入验证码:<input type="text" name="code"/>
<input type="submit" value="登录"><br></form>
<br><img th:src="@{/user/getImage}">
<a th:href="@{/user/registerview}">立即注册</a>
</form>
</body>
</html>
register.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http:www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>注册页面</h2>
<form th:action="@{/user/register}" method="post">
账号:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="立即注册"><br>
</form>
</body>
</html>
index.html
<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" xmlns:shrio="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>系统主页</title>
</head>
<body>
<h1>V0.1 系统主页</h1>
<h1><shiro:principal/></h1>
<ul>
<shiro:hasAnyRoles name="admin,user">
<li><a href="">用户管理</a>
<ul>
<ui>
<shiro:hasPermission name="user:create:*">
<li>创建</li>
</shiro:hasPermission>
<shiro:hasPermission name="user:delete:01">
<li>删处</li>
</shiro:hasPermission>
<shiro:hasPermission name="user:update:01">
<li>修改</li>
</shiro:hasPermission>
<shiro:hasPermission name="user:select:01">
<li>查询</li>
</shiro:hasPermission>
</ui>
</ul>
</li>
</shiro:hasAnyRoles>
<shiro:hasAnyRoles name="admin,product,order">
<shiro:hasPermission name="product:*:01">
<li><a href="">商品管理</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="order:*:01">
<li><a href="">订单管理</a></li>
</shiro:hasPermission>
<li><a href="">物流管理</a></li>
</shiro:hasAnyRoles>
</ul>
<hr>
<p><shiro:notAuthenticated>
</shiro:notAuthenticated></p>
<hr>
<p><shrio:authenticated/></p>
<a th:href="@{/user/logout}">退出</a>
</body>
</html>
- 修改web控制器
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService service;
/** * 处理身份验证 * @param username * @param password * @return */
@PostMapping("/login")
public String login(String username,String password,String code,HttpSession session){
String codes = (String) session.getAttribute("code");
try {
if(codes.equalsIgnoreCase(code)){
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username,password));
System.out.println("登陆成功");
return "redirect:/user/index";
}else{
throw new RuntimeException("验证码错误");
}
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名不存在");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}catch (RuntimeException e){
e.printStackTrace();
System.out.println(e.getMessage());
}
return "redirect:/user/loginview";
}
/** * 用户退出登录 * @return */
@GetMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/user/loginview";
}
@PostMapping("/register")
public String register(User user){
try {
service.save(user);
System.out.println("注册成功");
return "redirect:/user/loginview";
}catch (Exception e){
e.printStackTrace();
System.out.println("注册失败");
return "redirect:/user/registerview ";
}
}
@RequestMapping("/getImage")
public void getImage(HttpSession session, HttpServletResponse response){
try {
String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
session.setAttribute("code",verifyCode);
ServletOutputStream os = response.getOutputStream();
//验证码存入图片
response.setContentType("image/png");
VerifyCodeUtils.outputImage(220, 60, os , verifyCode);
} catch (IOException e) {
e.printStackTrace();
}
}
@RequestMapping("/loginview")
public String loginview(){
System.out.println("正在进入登录页面...");
return "login";
}
@RequestMapping("/registerview")
public String registerview(){
System.out.println("正在进入注册页面...");
return "register";
}
@RequestMapping("/index")
public String index(){
return "index";
}
}
- 添加springboot方言处理,否则shiro标签将失效!
在shiroConfig.java中添加
//加入spring boot的方言处理,否则页面标签将不起作用
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
- 测试
以xiaochen和zhangsan两种不同的身份登录系统!
xiaochen:
zhangsan:
比对数据库中xiaochen和zhansan对象的角色信息和权限
根据中间表可以知道xiaochen:admin,zhangsan:user,product
测试再通过角色权限表查看:
由此可见,虽然zhangsan拥有两个角色,但是他并没有拥有订单(order)权限,所以zhangsan没有订单管理选项!!
测试完成!!
参考学习:https://www.bilibili.com/video/BV1uz4y197Zm?p=26