Spring Boot整合Shiro
文章目录
- Spring Boot+Shiro
- 1 简介
- 2 环境搭建
- 2.1 开发环境
- 2.2 创建项目
- 2.3 项目文件结构
- 2.4 pom.xml
- 2.5 application.yml
- 3 数据库
- 3.1 数据库设计
- 3.2 数据库字段
- 4 类
- 4.1 po
- 4.2 mapper
- 4.3 config
- 4.4 realm
- 4.5 service
- 4.6 controller
- 5 页面
- 6 测试运行
- 6.1 普通用户登录
- 6.2 会员用户登录
Spring Boot+Shiro
Shiro这个Java安全框架我一直都想学会怎么去使用,但每次依照着别人的博客尝试把它配置到自己的项目中,总是出现各种问题,导致一直没有成功。经过不懈努力,这一次终于成功了!从零搭建整个项目,并通过一个简单的用户登录功能来进行说明!
源码github地址:https://github.com/Rhine404/shirodemo
1 简介
推荐阅读官方文档去认识Shiro,虽然不是中文的,但绝对是比看各种博客里重复度高、零零散散的资料好。
官方的简介如下:
Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Shiro是一款强大易用的Java安全框架,主要功能是进行认证、授权、加密、会话管理等。与Spring Security相比的话,Shiro是会更加轻量。
2 环境搭建
2.1 开发环境
- IDEA
- HTML/CSS/JavaScript + FreeMarker
- JDK8 + Maven + Spring Boot + Shiro
- druid + mybatis + Navicat + MySQL5.7
这里需要你有预备知识:
- 熟练使用IDEA或eclipse
- 了解Maven,知道如何配置settings.xml与maven仓库
- 了解Spring Boot,并会使用它搭建Web项目
- 了解FreeMarker/thymeleaf、yaml语法
虽然都是通过Maven构建项目,不过Spring Boot不再使用.xml文件对项目进行配置,所以Shiro的配置类ShiroConfig不太容易理解,对于刚开始接触Spring Boot的同学来说这是一个难点。
2.2 创建项目
这里使用IDEA通过Spring Initializr创建项目(当然也可以通过Maven自己添加所有依赖)。
我们下面要通过这个项目来搭建一个简单的用户系统,系统有两类用户——普通用户和VIP用户,通过shiro来完成登录、登出、权限验证功能,主要在于展示前后端的交互、后端与数据库的交互。
2.3 项目文件结构
2.4 pom.xml
新建的项目还需要再手动增加三个依赖项:druid数据库连接池,shiro依赖,log4j依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.rhine.blog</groupId>
<artifactId>shirodemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>shirodemo</name>
<description>Spring Boot with Shiro</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!-- Shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.5 application.yml
druid的配置会稍微多些,所以也附上了详细的注释,属性和值之间必须要有”:”号。
spring:
datasource:
url: jdbc:mysql://localhost:3306/lastpass?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
username: root
password: abc123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# 初始化时建立物理连接连接的个数
initialSize: 5
# 最小连接池数量
minIdle: 5
# 最大连接池数量
maxActive: 20
# 获取连接时最大等待时间(ms),即60s
maxWait: 60000
# 1.Destroy线程会检测连接的间隔时间;2.testWhileIdle的判断依据
timeBetweenEvictionRunsMillis: 60000
# 最小生存时间ms
minEvictableIdleTimeMillis: 600000
maxEvictableIdleTimeMillis: 900000
# 用来检测连接是否有效的sql
validationQuery: SELECT 1 FROM DUAL
# 申请连接时执行validationQuery检测连接是否有效,启用会降低性能
testOnBorrow: false
# 归还连接时执行validationQuery检测连接是否有效,启用会降低性能
testOnReturn: false
# 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,
# 执行validationQuery检测连接是否有效,不会降低性能
testWhileIdle: true
# 是否缓存preparedStatement,mysql建议关闭
poolPreparedStatements: false
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
freemarker:
suffix: .html
charset: utf-8
mvc:
# 配置静态资源映射路径,/public、/resources路径失效
static-path-pattern: /static/**
mybatis:
mapper-locations: classpath:mappers/*.xml
# 虽然可以配置这项来进行pojo包扫描,但其实我更倾向于在mapper.xml写全类名
type-aliases-package: com.rhine.blog.po
3 数据库
因为我们不仅仅要使用Shiro来进行认证,还要进行授权、加密等操作,所以我们要建立用户表以及权限管理表。
3.1 数据库设计
数据库有用户(user)、角色(role)、权限(permission)三个实体,除了实体表以外,为了实现表间用户与角色、角色与权限多对多的表间关系,所以产生了user_role、role_permission两张关系表。在下图中,使用红线将表的外键标记了出来,但为了方便并没有在表中创建外键,我们手动进行维护。
3.2 数据库字段
再简单介绍下数据库字段,user表中name是用户名,password是密码;role表中name是角色名(如user、vip);permission表中,name是权限名(如会员中心),url是实际的权限字段(user:vip)
4 类
4.1 po
Userbean.java
package com.rhine.blog.po;
/** * 用户类 */
public class UserBean implements Serializable {
private String id;
private String name;
private String password;
private Set<RoleBean> roles = new HashSet<>();
// 省略setter、getter方法
}
RoleBean.java
package com.rhine.blog.po;
/** * 角色类 */
public class RoleBean implements Serializable {
private String id;
private String name;
private Set<PermissionBean> permissions = new HashSet<>();
// 省略setter、getter方法
}
RoleBean.java
package com.rhine.blog.po;
/** * 权限类 */
public class PermissionBean implements Serializable {
private String id;
private String name;
private String url;
// 省略setter、getter方法
}
4.2 mapper
UserMapper.java
package com.rhine.blog.mapper;
public interface UserMapper {
// 查询用户信息
UserBean findByName(String name);
// 查询用户信息、角色、权限
UserBean findById(String id);
}
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.rhine.blog.mapper.UserMapper">
<resultMap id="userMap" type="UserBean">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="password" column="password"/>
<collection property="roles" ofType="RoleBean">
<id property="id" column="roleId"/>
<result property="name" column="roleName"/>
<collection property="permissions" ofType="PermissionBean">
<id property="id" column="permissionId"/>
<result property="name" column="permissionName"/>
<result property="url" column="permissionUrl"/>
</collection>
</collection>
</resultMap>
<select id="findByName" parameterType="String" resultType="UserBean">
SELECT id, name, password
FROM user
WHERE NAME = #{name}
</select>
<select id="findById" parameterType="String" resultMap="userMap">
SELECT user.id, user.name, user.password,
role.id as roleId, role.name as roleName,
permission.id as permissionId,
permission.name as permissionName,
permission.url as permissionUrl
FROM user, user_role, role, role_permission, permission
WHERE user.id = #{id}
AND user.id = user_role.user_id
AND user_role.role_id = role.id
AND role.id = role_permission.role_id
AND role_permission.permission_id = permission.id
</select>
</mapper>
因为在application.yml中配置了 type-aliases-package: com.rhine.blog.po,所以resultType可以不用写全类名。添加新的查询时,一定要区分清楚resultMap和resultType,否则出错不容易发现。
4.3 config
DruidConfig.java
package com.rhine.blog.config;
/** * Druid配置类 */
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean(destroyMethod="close", initMethod = "init")
public DataSource druid(){
return new DruidDataSource();
}
/** * 配置监控服务器 **/
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean bean = new ServletRegistrationBean(
new StatViewServlet(), "/druid/*");
Map<String,String> initParams = new HashMap<>();
// druid后台管理员用户
initParams.put("loginUsername","admin");
initParams.put("loginPassword","123456");
// 是否能够重置数据
initParams.put("resetEnable", "false");
bean.setInitParameters(initParams);
return bean;
}
/** * 配置web监控的过滤器 **/
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean(
new WebStatFilter());
// 添加过滤规则
bean.addUrlPatterns("/*");
Map<String,String> initParams = new HashMap<>();
// 忽略过滤格式
initParams.put("exclusions","*.js,*.css,*.icon,*.png,*.jpg,/druid/*");
bean.setInitParameters(initParams);
return bean;
}
}
ShiroConfig.java
package com.rhine.blog.config;
/** * Shiro配置类 */
@Configuration
public class ShiroConfig {
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher =
new HashedCredentialsMatcher();
//指定加密方式为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
//加密次数
credentialsMatcher.setHashIterations(1024);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
@Bean("userRealm")
public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher")
HashedCredentialsMatcher matcher) {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(matcher);
return userRealm;
}
@Bean
public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager")
DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置 SecurityManager
bean.setSecurityManager(securityManager);
// 设置登录成功跳转Url
bean.setSuccessUrl("/main");
// 设置登录跳转Url
bean.setLoginUrl("/toLogin");
// 设置未授权提示Url
bean.setUnauthorizedUrl("/error/unAuth");
/** * anon:匿名用户可访问 * authc:认证用户可访问 * user:使用rememberMe可访问 * perms:对应权限可访问 * role:对应角色权限可访问 **/
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/login","anon");
filterMap.put("/user/index","authc");
filterMap.put("/vip/index","roles[vip]");
filterMap.put("/druid/**", "anon");
filterMap.put("/static/**","anon");
filterMap.put("/**","authc");
filterMap.put("/logout", "logout");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
/** * 注入 securityManager */
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(
HashedCredentialsMatcher hashedCredentialsMatcher) {
DefaultWebSecurityManager securityManager =
new DefaultWebSecurityManager();
// 关联realm.
securityManager.setRealm(userRealm(hashedCredentialsMatcher));
return securityManager;
}
}
4.4 realm
UserRealm.java
package com.rhine.blog.realm;
/** * 自定义Realm,实现授权与认证 */
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/** * 用户授权 **/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
System.out.println("===执行授权===");
Subject subject = SecurityUtils.getSubject();
UserBean user = (UserBean)subject.getPrincipal();
if(user != null){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 角色与权限字符串集合
Collection<String> rolesCollection = new HashSet<>();
Collection<String> premissionCollection = new HashSet<>();
// 读取并赋值用户角色与权限
Set<RoleBean> roles = user.getRole();
for(RoleBean role : roles){
rolesCollection.add(role.getName());
Set<PermissionBean> permissions = role.getPermissions();
for (PermissionBean permission : permissions){
premissionCollection.add(permission.getUrl());
}
info.addStringPermissions(premissionCollection);
}
info.addRoles(rolesCollection);
return info;
}
return null;
}
/** * 用户认证 **/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("===执行认证===");
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
UserBean bean = userService.findByName(token.getUsername());
if(bean == null){
throw new UnknownAccountException();
}
ByteSource credentialsSalt = ByteSource.Util.bytes(bean.getName());
return new SimpleAuthenticationInfo(bean, bean.getPassword(),
credentialsSalt, getName());
}
// 模拟Shiro用户加密,假设用户密码为123456
public static void main(String[] args){
// 用户名
String username = "rhine";
// 用户密码
String password = "123456";
// 加密方式
String hashAlgorithName = "MD5";
// 加密次数
int hashIterations = 1024;
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
Object obj = new SimpleHash(hashAlgorithName, password,
credentialsSalt, hashIterations);
System.out.println(obj);
}
}
4.5 service
UserService.java
package com.rhine.blog.service;
/** * UserService抽象接口 */
public interface UserService {
UserBean findByName(String name);
}
UserServiceImpl.java
package com.rhine.blog.service.impl;
/** * UserService实现类 */
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public UserBean findByName(String name) {
// 查询用户是否存在
UserBean bean = userMapper.findByName(name);
if (bean != null) {
// 查询用户信息、角色、权限
bean = userMapper.findById(bean.getId());
}
return bean;
}
}
4.6 controller
MainController.java
package com.rhine.blog.controller;
/** * 用户登录、登出、错误页面跳转控制器 */
@Controller
public class MainController {
@RequestMapping("/main")
public String index(HttpServletRequest request, HttpServletResponse response){
response.setHeader("root", request.getContextPath());
return "index";
}
@RequestMapping("/toLogin")
public String toLogin(HttpServletRequest request, HttpServletResponse response){
response.setHeader("root", request.getContextPath());
return "login";
}
@RequestMapping("/login")
public String login(HttpServletRequest request, HttpServletResponse response){
response.setHeader("root", request.getContextPath());
String userName = request.getParameter("username");
String password = request.getParameter("password");
// 1.获取Subject
Subject subject = SecurityUtils.getSubject();
// 2.封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
// 3.执行登录方法
try{
subject.login(token);
return "redirect:/main";
} catch (UnknownAccountException e){
e.printStackTrace();
request.setAttribute("msg","用户名不存在!");
} catch (IncorrectCredentialsException e){
request.setAttribute("msg","密码错误!");
}
return "login";
}
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
if (subject != null) {
subject.logout();
}
return "redirect:/main";
}
@RequestMapping("/error/unAuth")
public String unAuth(){
return "/error/unAuth";
}
}
UserController.java
package com.rhine.blog.controller;
/** * 用户页面跳转 */
@Controller
public class UserController {
/** * 个人中心,需认证可访问 */
@RequestMapping("/user/index")
public String add(HttpServletRequest request){
UserBean bean = (UserBean) SecurityUtils.getSubject().getPrincipal();
request.setAttribute("userName", bean.getName());
return "/user/index";
}
/** * 会员中心,需认证且角色为vip可访问 */
@RequestMapping("/vip/index")
public String update(){
return "/vip/index";
}
}
5 页面
login.html——登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>用户登录</h1>
<hr>
<form id="from" action="${root!}/login" method="post">
<table>
<tr>
<td>用户名</td>
<td>
<input type="text" name="username" placeholder="请输入账户名"/>
</td>
</tr>
<tr>
<td>密码</td>
<td>
<input type="password" name="password" placeholder="请输入密码"/>
</td>
</tr>
<tr>
<td colspan="2">
<font color="red">${msg!}</font>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="登录"/>
<input type="reset" value="重置"/>
</td>
</tr>
</table>
</form>
</body>
</html>
index.html——首页
<!DOCTYPE html>
<html>
<head>
<title>首页</title>
</head>
<body>
<h1>首页</h1>
<hr>
<ul>
<li><a href="user/index">个人中心</a></li>
<li><a href="vip/index">会员中心</a></li>
<li><a href="logout">退出登录</a></li>
</ul>
</body>
</html>
/user/index.html——用户中心
<!DOCTYPE html>
<html>
<head>
<title>用户中心</title>
</head>
<body>
<h1>用户中心</h1>
<hr>
<h1>欢迎${userName!},这里是用户中心</h1>
</body>
</html>
/vip/index.html——会员中心
<!DOCTYPE html>
<html>
<head>
<title>会员中心</title>
</head>
<body>
<h1>会员中心</h1>
<hr>
<h1>欢迎来到<font color="red">会员中心</font></h1>
</body>
</html>
/error/unAuth.html——未授权提示页面
<!DOCTYPE html>
<html>
<head>
<title>未授权提示</title>
</head>
<body>
<h1>您还不是<font color="red">会员</font>,没有权限访问这个页面!</h1>
</body>
</html>
还没有评论,来说两句吧...