Shiro是一款开源安全框架,Shiro提供的Api可以用于 :身份验证(登录)、授权-访问控制、会话管理、加密。
Shiro官网:
目前Shiro和SpringSecurity一样经常被用与和Spring的各种框架整合使用
学习Shiro主要
Subject:当前用户
Shiro SecurityManager:安全管理器,管理着所有 Subject
Realm:域,放着用户认证和授权的安全信息,通常由我们自己自定义一个域,然后注入给SecurityManager,SecurityManager再判断该用户的权限。
做个小Demo进行理解一下
SpringBoot+Mybatis+thymeleaf+mysql
- 首页先创建一个Spirngboot的项目,导入依赖。
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
- 创建数据库:
/*
SQLyog Ultimate v12.08 (64 bit)
MySQL - 5.7.33 : Database - db_shiro
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`db_shiro` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `db_shiro`;
/*Table structure for table `permission` */
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`permissionname` varchar(50) NOT NULL COMMENT '权限名',
`role_id` int(11) DEFAULT NULL COMMENT '外键关联role',
PRIMARY KEY (`id`),
KEY `role_id` (`role_id`),
CONSTRAINT `permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
/*Data for the table `permission` */
insert into `permission`(`id`,`permissionname`,`role_id`) values (1,'user:*',1),(2,'del:*',2);
/*Table structure for table `role` */
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`rolename` varchar(20) DEFAULT NULL COMMENT '角色名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Data for the table `role` */
insert into `role`(`id`,`rolename`) values (1,'admin'),(2,'user'),(3,'user1');
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户主键',
`username` varchar(20) NOT NULL COMMENT '用户名',
`password` varchar(20) NOT NULL COMMENT '密码',
`role_id` int(11) DEFAULT NULL COMMENT '外键关联role表',
PRIMARY KEY (`id`),
KEY `role_id` (`role_id`),
CONSTRAINT `user_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Data for the table `user` */
insert into `user`(`id`,`username`,`password`,`role_id`) values (1,'ctr1','123456',1),(2,'ctr2','123456',2),(3,'ctr3','123456',3);
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
- 创建这3个表的实体类Permission、Role、User
@Data
public class Permission {
private Integer id;
private String permissionName;
private Integer roleId;
}
@Data
public class Role {
private Integer id;
private String roleName;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String userName;
private String passWord;
private Integer roleId;
public User(String userName, String passWord){
this.userName = userName;
this.passWord = passWord;
}
}
- 创建Mapper和Mapper.xml、Service后续认证和授权需要查数据库
@Repository
@Mapper
public interface UserMapper {
Set<String> getRoles(String userName);
Set<String> getPermissions(String userName);
// @Select("select * from db_shiro.t_user where username = #{userName}")
User getByUserName(String userName);
}
<?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.ctr.mapper.UserMapper">
<select id="getRoles" resultType="String" parameterType="String">
select r.rolename from db_shiro.user u, db_shiro.role r where u.username= #{userName} and u.role_id = r.id;
</select>
<select id="getPermissions" resultType="String" parameterType="String">
select p.permissionname from db_shiro.user u, db_shiro.permission p where u.username=#{userName} and u.role_id = p.role_id;
</select>
<select id="getByUserName" resultType="User" parameterType="String">
select * from db_shiro.user where username = #{userName};
</select>
</mapper>
public interface UserService {
Set<String> getRoles(String userName);
Set<String> getPermissions(String userName);
User getByUserName(String userName);
}
package com.ctr.service;
import com.ctr.mapper.UserMapper;
import com.ctr.pojo.Permission;
import com.ctr.pojo.Role;
import com.ctr.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Override
public Set<String> getRoles(String userName) {
return userMapper.getRoles(userName);
}
@Override
public Set<String> getPermissions(String userName) {
return userMapper.getPermissions(userName);
}
@Override
public User getByUserName(String userName) {
return userMapper.getByUserName(userName);
}
}
- 自定义Realm,继承AuthorizingRrealm,重写doGetAuthorizationInfo(授权)、doGetAuthenticationInfo(认证)方法
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取用户名
String username = (String) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 给该用户设置角色信息
authorizationInfo.setRoles(userService.getRoles(username));
// 给该用户设置权限信息
authorizationInfo.setStringPermissions(userService.getPermissions(username));
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 根据 authenticationToken 获取用户名
String username = (String) authenticationToken.getPrincipal();
// 根据用户名从数据库中查找该用户
User user = userService.getByUserName(username);
if (user != null){
// 把当前用户存到Session中
SecurityUtils.getSubject().getSession().setAttribute("user",user);
AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getUserName(),user.getPassWord(), "myRealm");
return authcInfo;
}else {
return null;
}
}
}
- @Configuration 配置Shiro,注入自定义Realm、SecurityManager 、shiroFilter,项目启动时,就注入到环境中
@Configuration
public class ShiroConfig {
private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
@Bean
public MyRealm myAuthRealm(){
MyRealm myRealm = new MyRealm();
logger.info("========myRealm注册完成=======");
return myRealm;
}
@Bean
public SecurityManager securityManager(){
DefaultSecurityManager securityManager = new DefaultWebSecurityManager(myAuthRealm());
logger.info("====securityManager注册完成====");
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
// 定义 shiroFactoryBean
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置自定义的 securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置默认登录地址,登录认证失败会访问该地址
shiroFilterFactoryBean.setLoginUrl("/user/login");
// 设置成功之后要跳转的地址
shiroFilterFactoryBean.setSuccessUrl("/success");
// 设置未授权界面
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
// 拦截器配置
Map<String,String> filterChainMap = new LinkedHashMap<>();
// 配置放行资源,anon 表示放行
filterChainMap.put("/css/**", "anon");
filterChainMap.put("/imgs/**", "anon");
filterChainMap.put("/js/**", "anon");
filterChainMap.put("/swagger-*/**", "anon");
filterChainMap.put("/swagger-ui.html/**", "anon");
filterChainMap.put("/user/login","anon");
// 以"/user/admin/开头的用户需要身份验证, authc表示要进行身份认证
filterChainMap.put("/user/admin*","authc");
// “/user/dog” 开头的用户需要角色认证,是“admin”才允许
filterChainMap.put("/user/dog*/**", "roles[admin]");
// “/user/cat” 开头的用户需要权限认证,是“user:create”才允许
filterChainMap.put("/user/cat*/**", "perms[\"user:create\"]");
// 配置logout过滤器
filterChainMap.put("/logout","logout");
// 设置shiroFilterFactoryBean 的setFilterChainDefinitionMap
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
logger.info("========shiroFilterFactoryBean注册完成=======");
return shiroFilterFactoryBean;
}
}
- Controller编写接口,当用户请求登录接口(user/login)时,根据前端传来的账号密码创建一个token,通过SecurityUtils.getSubject()创建当前用户主体,subject.login(token)来到自定义的Realm中的doGetAuthenticationInfo开始认证
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/admin")
public String admin(HttpServletRequest request) {
Object user = request.getSession().getAttribute("user");
return "success";
}
@RequestMapping("/dog")
public String dog(HttpServletRequest request) {
return "success";
}
@RequestMapping("/cat")
public String cat(HttpServletRequest request) {
return "success";
}
@RequestMapping("/login")
public String login(User user, HttpServletRequest request) {
// 根据用户名和密码创建 Token
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassWord());
// 获取 subject 认证主体
Subject subject = SecurityUtils.getSubject();
try{
// 开始认证,这一步会跳到我们自定义的 Realm 中
subject.login(token);
request.getSession().setAttribute("user", user);
return "success";
}catch(Exception e){
e.printStackTrace();
request.getSession().setAttribute("user", user);
request.setAttribute("error", "用户名或密码错误!");
return "login";
}
}
@RequestMapping("/logout")
public String logOut(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
}
application.yml
server:
port: 8086
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_shiro?charset=true&characterEncoding=utf8
password: 123456
username: root
driver-class-name: com.mysql.cj.jdbc.Driver
web:
resources:
static-locations: classpath:/templates/
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.ctr.pojo
- 编写html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/XSL/Transform">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${error}"></p>
<form th:action="@{/user/login}" method="post">
<p>账号:<input type="text" name="userName"></p>
<p>密码:<input type="password" name="passWord"></p>
<p><input type="submit" value="登录"></p>
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>登录成功</title>
</head>
<body>
<h1>恭喜你登录成功</h1>
<a th:href="@{/user/logout}">注销</a>
</body>
</html>
- 测试
当请求http://localhost:8086/user/admin[cat/dog]时,因为在ShiroConfig中配置了未登录重定向到/user/login,此处有个bug因为我直接走了这个请求,所以请求携带error。
输入ctr1,123456登录成功
成功后均可访问http://localhost:8086/user/dog、http://localhost:8086/user/cat
注销后回到登录页面,用ctr2,123456登录成功后访问http://localhost:8086/user/dog、http://localhost:8086/user/cat,重定向到未授权页面
一个简单的登录授权的小Demo完成了。