Springboot搭建Shiro
Shiro是一个Apach旗下的Java的安全框架。相比较Spring Security,它比较轻量级,因此在国内的使用率比较高。
代码的编写
为了帮助更好的理解shiro的功能,以及快速上手。但这里仅仅包含了登录拦截和权限验证,不包括角色相关功能。待我更深入的Shiro的学习之后再做更新。
代码的功能:不同用户拥有不同的权限。,角色需要登录后才能进入功能界面。
该代码是Springboot+Mybatis+Shiro的项目,前端使用thymeleaf。
1.目录结构
2.pom.xml文件的关键代码的引入
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<shiro.version>1.4.0</shiro.version>
</properties>
<dependencies>
<!--导入thymeleaf模板-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--导入web支持:SpringMVC开发支持,Servlet相关程序-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--test测试的支持-->
<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>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
</dependencies>
3.application.yml文件的代码
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
# 本地ip:端口号/数据库名称
url: jdbc:mysql://127.0.0.7:3306/shirodemo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
#druid数据连接池的参数配置
type: com.alibaba.druid.pool.DruidDataSource
druid:
max-active: 30
initial-size: 5
min-idle: 5
max-wait: 60000
thymeleaf:
cache: false
mybatis:
configuration:
#使用camel规则
map-underscore-to-camel-case: true
#打印mybatis日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
use-column-label: true
#mapper.xml文件映射位置
mapper-locations: classpath:mapper/*.xml
#扫描的实体类的位置
type-aliases-package: cn.study.shirodemo.bean
4.用户类的建立
对应User.Class类,与用户表字段对应
public class User implements Serializable {
private static final long serialVersionUID = -6675401285085356381L;
private Integer id;
private String username;
private String password;
private String perms;
public User() {
}
public Integer getId() {
return id;
}
public void setId(Integer 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 String getPerms() {
return perms;
}
public void setPerms(String perms) {
this.perms = perms;
}
}
5.Dao层
UserMapper.Java文件
public interface UserMapper {
User getUserByName(String username);
User getUserById(Integer id);
}
UserMapper.xml文件
这里放在resources的mapper目录下
<?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="cn.study.shirodemo.mapper.UserMapper">
<select id="getUserByName" resultType="cn.study.shirodemo.bean.User">
SELECT *
FROM user where username = #{username}
</select>
<select id="getUserById" resultType="cn.study.shirodemo.bean.User">
SELECT *
FROM user where id = #{id}
</select>
</mapper>
若提示错误数据库相关根据下面图示修改路径
6.Service层
UserService
public interface UserService {
User getUserByName(String username);
User getUserById(Integer id);
}
UserServiceImpl实现类
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User getUserByName(String username) {
User user = userMapper.getUserByName(username);
return user;
}
@Override
public User getUserById(Integer id) {
User user = userMapper.getUserById(id);
return user;
}
}
7.UserController
@Controller
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/toLogin")
public String toLogin() {
return "/login";
}
@RequestMapping("/toMian")
public String toMian() {
return "/user/main";
}
@RequestMapping("/toAdd")
public String toAdd() {
return "/user/add";
}
@RequestMapping("/toUpdate")
public String toUpdate() {
return "/user/update";
}
@RequestMapping("/unAuth")
public String unAuth() {
return "/unAuth";
}
//===============================上面是页面跳转功能的代码=================================
@RequestMapping("/login")
public String login(String username, String password, Model model) {
//获取subject
Subject subject = SecurityUtils.getSubject();
//封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//执行登录操作
try {
subject.login(token);
//登录成功
return "/user/main";
} catch (UnknownAccountException e) {
//账户未知错误
model.addAttribute("msg", "用户不存在");
return "/login";
} catch (IncorrectCredentialsException e) {
//认证信息不正确错误
model.addAttribute("msg", "密码不正确");
return "/login";
}
}
}
注意
Subject是import org.apache.shiro.subject.Subject;
包下的
启动类
9.重头戏:配置UserRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 执行授权逻辑
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权逻辑");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
//通常权限表和用户表是分开的,这里为了简洁,使用同一张表
//添加匹配的字符串
String perms = user.getPerms();
String[] strs = perms.split(",");
for (String s : strs) {
info.addStringPermission(s);
}
return info;
}
/**
* 执行认证逻辑
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证逻辑");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
User user = userService.getUserByName(username);
if (user == null) {
return null;//shiro会抛出UnknownAccountException
}
//判断密码
return new SimpleAuthenticationInfo(user, user.getPassword(), "");
}
}
①登录操作,执行的是doGetAuthenticationInfo
方法(执行认证逻辑部分)
首先从参数token中获取用户名和密码后,通过mybatis从数据库找出对应的user对象。
若不存在user为null,null作为返回值会触发controller代码中的UnknownAccountException
异常。
若不为null,返回new SimpleAuthenticationInfo(user, user.getPassword(), "");
user
是Principal
对象,是权限验证时使用的参数。
user.getPassword()
是密码,shiro会匹配token中的密码,错误时会抛出IncorrectCredentialsException
异常。
参数三这篇文章不做研究。
②授权认证,执行的是doGetAuthorizationInfo
方法(执行授权逻辑部分)
doGetAuthorizationInfo
是执行权限的部分,通过User user = (User) subject.getPrincipal();
将认证逻辑中的user
参数保存过来。
info.addStringPermission(s)
是添加授权逻辑,下面会介绍如何使用。
10.编写ShiroConfig类
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilterFact
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("webSecurityManager") DefaultWebSecurityManager manager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(manager);
//添加Shiro内置过滤器
/**
* 内置过滤器,可以实现权限相关的拦截器
* 常用的过滤器:
* anon:无需认证(登录)可以访问
* authc:必须认证才能访问
* user:如果使用remeberMe的功能可以直接访问
* perms:该资源必须得到资源权限才能访问
* role:该资源必须得到角色权限才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/login", "anon");//无需认证就能访问
filterMap.put("/toLogin", "anon");//无需认证就能访问
//注意拦截请求默认是拦截的,需要用anon来放行
//拦截后,会自动跳转到未授权的页面
filterMap.put("/toAdd", "perms[user:add]");
filterMap.put("/toUpdate", "perms[user:update]");
filterMap.put("/*", "authc");//拦截
//调整成登录页面
bean.setLoginUrl("/toLogin");
//设置未授权的页面
bean.setUnauthorizedUrl("/unAuth");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
/**
* 创建DefaultWebSecurityManager
*/
@Bean(name = "webSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//关联realm
manager.setRealm(userRealm);
return manager;
}
@Bean(name = "userRealm")
public UserRealm getRealm() {
return new UserRealm();
}
}
defaultWebSecurityManager
方法是将我们自定义的UserRealm添加到Shiro过滤器配置中
这里我们深入研究getShiroFilterFactoryBean
方法
当方法设置好安全管理器后,需要添加过滤内容
内置过滤器,可以实现权限相关的拦截器
常用的过滤器:
anon:无需认证(登录)可以访问
authc:必须认证才能访问
user:如果使用remeberMe的功能可以直接访问
perms:该资源必须得到资源权限才能访问
role:该资源必须得到角色权限才能访问
注意
添加过程中:一定要在最后添加
filterMap.put("/*", "authc");//拦截
否则在该段代码之后添加的过滤内容会无效。
这里还有个小事项:就是添加无需过滤的内容时,一定要将登录页面的所有请求添加全,否则浏览器会提示错误,而客户端并没有任何错误提示。(当时困了我很久 = =!)
filterMap.put("/toAdd", "perms[user:add]");
filterMap.put("/toUpdate", "perms[user:update]");
这部分代码是权限管理时使用的代码。Shiro会去匹配[]
内的内容是否一致。
匹配的对象就是UserRealm类中getShiroFilterFactoryBean
的info.addStringPermission(s);
我这里使用的是下表的方式,再用Split方法循环addStringPermission
方法放入。
11.简单搭建前端页面
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
登录页面
<div>
<form action="/login" method="post">
<div>
<label>用户名:</label>
<input type="text" id="username" name="username">
</div>
<div>
<label>密码:</label>
<input type="password" id="password" name="password">
</div>
<input type="submit" value="登录">
</form>
<div th:text="${msg}" color="red"></div>
</div>
</body>
</html>
unAuth.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
您没有权限
<a href="/toLogin">点击返回到登录页面</a>
</body>
</html>
add.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
添加用户
</body>
</html>
update.html与之类似
main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
<div>
<a href="/toAdd">添加功能</a>
</div>
<div>
<a href="/toUpdate">修改功能</a>
</div>
</body>
</html>
大功告成!启动项目体验一下吧!