发布时间:2023-03-17 13:30
Shiro 是一个开源的轻量级的 Java 安全框架,同时支持 Java SE 和 Java EE 项目,其主要提供身份认证、授权、密码管理以及会话管理等功能。本文将介绍 Spring Boot 整合 Shiro 并实现基本的认证授权功能。
创建一个 Spring Boot 项目(默认添加 web 依赖),然后导入如下依赖:
<!--shiro-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
<!--数据库-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
其中,shiro 的依赖为 shiro-spring
,其他的依赖是需要用到的相关组件。
根据用户与角色的关系,简单创建如下三张表:
对应的 SQL 如下:
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`role_id` int(10) NOT NULL AUTO_INCREMENT,
`role_name` varchar(30) NOT NULL,
`role_detail` varchar(30) NOT NULL,
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='角色表';
insert into `role`(`role_id`,`role_name`,`role_detail`) values
(1,'sadmin','超级管理员'),
(2,'admin','管理员'),
(3,'user','普通用户');
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`pwd` varchar(30) NOT NULL,
`enabled` int(10) NOT NULL,
`locked` int(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='用户表';
insert into `user`(`id`,`name`,`pwd`,`enabled`,`locked`) values
(1,'root','123456',1,0),
(2,'admin','123456',1,0),
(3,'study','123456',1,0);
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`user_id` int(10) NOT NULL,
`role_id` int(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='用户-角色表';
insert into `user_role`(`id`,`user_id`,`role_id`) values
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);
在 application.yaml
中进行配置:
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/springboot-data?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
type-aliases-package: com.study.pojo
mapper-locations: classpath:mapper/*.xml
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private Integer role_id;
private String role_name;
private String role_detail;
}
User 中需包含一个 List,存放多个权限
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private String pwd;
private Integer enabled;
private Integer locked;
private List<Role> roles;
}
UserMapper 提供两个查询接口:根据名字查询用户;根据 id 查询用户的所有权限
@Repository
@Mapper
public interface UserMapper {
User queryUserByName(@Param("name") String name);
List<Role> queryRolesByUid(@Param("id") int id);
}
UserMapper.xml
<mapper namespace="com.study.mapper.UserMapper">
<select id="queryUserByName" resultType="user">
select * from user where name = #{name};
select>
<select id="queryRolesByUid" resultType="role">
select *
from role r, user_role ur
where ur.role_id = r.role_id and ur.user_id = #{id};
select>
mapper>
UserService 中封装 UserMapper 的两个查询方法:
public interface UserService {
User queryUserByName(String name);
List<Role> queryRolesByUid(int id);
}
实现类 UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User queryUserByName(String name) {
return userMapper.queryUserByName(name);
}
@Override
public List<Role> queryRolesByUid(int id) {
return userMapper.queryRolesByUid(id);
}
}
在 controller 中完成重定向、登录、注销等请求
重点关注的是其中的 login
方法,整个登录认证过程如下:
UsernamePasswordToken
中,然后调用 subject.login(token)
方法开启登录UserRealm(自定义)
中的 doGetAuthenticationInfo()
方法中,其中通过传入的 token
参数来获取用户输入的信息,此时在数据库中根据用户名查询用户,如果查询出的用户不为空,则返回一个 SimpleAuthenticationInfo
对象,在其中将数据库中的用户信息传入,shiro
会自动地帮我们完成登录认证的功能。try
语句块中;否则根据异常类型返回相应的错误信息。@Controller
public class RouterController {
@GetMapping("/{url}")
public String redirect(@PathVariable("url") String url) {
return url;
}
@PostMapping("/login")
public String login(@RequestParam("username") String name, @RequestParam("password") String pwd, Model model) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(name, pwd);
try {
subject.login(token);
return "index";
} catch (UnknownAccountException e) {
model.addAttribute("msg", "用户名错误!");
return "login";
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg", "密码错误!");
return "login";
}
}
@GetMapping("/unauth")
@ResponseBody
public String unauth() {
return "未授权,无法访问!";
}
@GetMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
}
UserReaml
是我们自定义的,在其中完成验证和授权的逻辑。
doGetAuthenticationInfo()
方法,其中根据前端输入的用户名和密码,先通过用户名查找用户,如果用户名不为空,则再将认证工作交给 shiro
实现,对应代码中的 return new SimpleAuthenticationInfo(user, user.getPwd(), getName());
doGetAuthorizationInfo()
方法,我们在其中获取到当前登录的用户对象,然后在数据库中查找出其所有的权限,存入 Set 集合中,最后用其构造 SimpleAuthorizationInfo
对象并返回,即可完成授予用户权限的操作。public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
Set<String> roles = new HashSet<>();
List<Role> roleList = userService.queryRolesByUid(user.getId());
for (Role role : roleList) {
roles.add(role.getRole_name());
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = userService.queryUserByName(token.getUsername());
if (user != null) {
Subject subject = SecurityUtils.getSubject();
subject.getSession().setAttribute("user", user);
return new SimpleAuthenticationInfo(user, user.getPwd(), getName());
}
return null;
}
}
创建 ShiroConfig
类,通过 @Configuration
开启 Shiro,不需要使用 Shiro 时直接将该注解删除即可,其中需要返回 3 个 Bean 对象,分别为 Realm
、DefaultWebSecurityManager
、ShiroFilterFactoryBean
。
Realm
:其中写的是认证与授权的逻辑。即通过名称查询用户、通过 id 查询用户所拥有的角色权限,我们只需将这些信息返回给 Shiro 即可;DefaultWebSecurityManager
:负责接收我们传入的 Realm,然后还要将其返回并注入到最终的 ShiroFilterFactoryBean
中;ShiroFilterFactoryBean
:其中设置我们要拦截的请求所需要的角色权限等,比如访问哪个页面需要什么角色、权限,全都在其中定义。常用的认证授权规则如下:
参数 | 含义 |
---|---|
anon | 无需认证即可访问 |
authc | 登录后可访问 |
perms | 拥有某个权限才可访问 |
role | 拥有某个角色才可访问 |
ShiroConfig 的完整代码如下:
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 设置请求权限
Map<String, String> map = new Hashtable<>();
map.put("/sadmin/**", "roles[sadmin]");
map.put("/admin/**", "roles[admin]");
map.put("/user", "authc");
factoryBean.setFilterChainDefinitionMap(map);
// 设置拦截后进入的登录界面
factoryBean.setLoginUrl("/login");
// 设置未授权的访问失败页面
factoryBean.setUnauthorizedUrl("/unauth");
return factoryBean;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
@Bean(name = "userRealm")
public UserRealm getUserRealm() {
return new UserRealm();
}
}
代码解释:
@Bean
注解先将我们自定义的 UserRealm
注入到 IOC 容器中DefaultWebSecurityManager
,通过 setRealm
方法将 1 中的 userRealm
注入到其中,同时也要加上 @Bean
注解ShiroFilterFactoryBean
,其中设置我们要拦截的请求所需要的角色权限等:
setSecurityManager
方法将 2 中的 DefaultWebSecurityManager
注入到其中;setFilterChainDefinitionMap
将存放请求权限配置的 Map 添加到 Shiro 的过滤器中;setLoginUrl
设置拦截后进入的登录界面,setUnauthorizedUrl
设置用户权限不足导致访问失败后的跳转请求。ps:@Bean
注解默认配置的是方法名,可通过 name
进行自定义,然后在接收参数时通过 @Qualifier()
获取
这里定义的权限如下:1. 拥有 sadmin 角色才可访问 sadmin.html;2. 拥有 admin 角色才可访问 admin.html;3. 登录后才可访问 user.html
在 templates 下创建如下几个界面:
这样 controller 中的 /{url}
请求就可以根据视图名称动态返回对应的页面
其中,index.html 如下:
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>首页h1>
<p th:if="${session.user != null}">
欢迎用户:<span th:text="${session.user.name}">span> <br>
<a href="/logout">注销a>
p>
<a th:href="@{/sadmin}">超级管理员入口a>
p>
<p>
<a th:href="@{/admin}">管理员入口a>
p>
<p>
<a th:href="@{/user}">普通用户入口a>
p>
body>
html>
运行项目,三个超链接分别对应三种角色的请求,点击后将拦截并跳转到我们自定义的登录界面
登录用户 admin,由于其不具备 sadmin 角色,故第一张图中访问的结果是失败的;其具有 admin 角色则访问 admin 界面成功;user 界面登录后即可访问。
以上就是 Spring Boot 项目整合 Shiro 的基本用法。
CTC Algorithm Explained Part 2:Decoding the Network(CTC算法详解之解码篇)
《动手学深度学习-pytorch》书中定义函数后加#@save的含义
一个2022本科生的秋招总结 (大疆、Arm、小米、荣耀、美团、联发科等)
机器学习之回归模型预测性能评估指标(RMSE、MSE、MAE、MAPE、SMAPE、R^2 Score、R^2 )
【GoggLeNet 的 inception 结构】 与 【ResNet 的 Residual 结构】 tensor拼接方式的区别
2022 3月软件测试面试题(800道)【附带答案】持续更新...
Android的SharePreferences存储的案例(qq账号存储)
Could not find org.jetbrains.kotlin:kotlin-stdlib