Springboot整合shiro之用户验证
搭建springboot项目
首先,搭建一个springboot项目,持久层采用Mybaits,数据库使用MySQL8.0。
创建数据库
数据库的创建需根据该项目需要验证的权限模型来,该项目使用简单的权限模型来验证shiro的基础功能,用户-角色-权限模型,示意图如下:
也就是,总共有5张表,数据库脚本如下:
/*
DROP DATABASE IF EXISTS springboot_shiro_practice;
CREATE DATABASE springboot_shiro_practice DEFAULT CHARACTER SET UTF8;
*/
/*
用户表
*/
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT'自增id',
name VARCHAR(32) NOT NULL COMMENT'用户名',
password VARCHAR(128) NOT NULL COMMENT'密码',
remark VARCHAR(64) COMMENT '备注'
) COMMENT='用户表';
INSERT INTO users(name,password,remark) VALUES("苍老师",MD5("111"),"你懂的");
INSERT INTO users(name,password,remark) VALUES("小炮同学",MD5("666"),"无");
/*
角色表
*/
DROP TABLE IF EXISTS roles;
CREATE TABLE roles (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT'自增id',
name VARCHAR(64) NOT NULL COMMENT'角色名称',
remark VARCHAR(64) COMMENT'备注'
) COMMENT = '角色表';
INSERT INTO roles (name,remark) VALUES ("TEACHER",'老师');
INSERT INTO roles (name,remark) VALUES ("STUDENT",'学生');
/*
权限表
*/
DROP TABLE IF EXISTS privilege;
CREATE TABLE privilege (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT'自增id',
name VARCHAR(64) NOT NULL COMMENT'权限名称',
remark VARCHAR(64) COMMENT'备注'
) COMMENT = '权限表';
INSERT INTO privilege (name,remark) VALUES("DRINKING_WINE","喝酒");
INSERT INTO privilege (name,remark) VALUES("SMOKING","抽烟");
INSERT INTO privilege (name,remark) VALUES("PLAY_GAME","玩游戏");
/*
用户-角色关联表
*/
DROP TABLE IF EXISTS user_role;
CREATE TABLE user_role (
user_id INT NOT NULL COMMENT '用户id',
role_id INT NOT NULL COMMENT '角色id'
) COMMENT = '用户角色关联表';
INSERT INTO user_role (user_id,role_id) VALUES(1,1);
INSERT INTO user_role (user_id,role_id) VALUES(2,2);
/*
角色-权限表
*/
DROP TABLE IF EXISTS role_privilege;
CREATE TABLE role_privilege(
role_id INT NOT NULL COMMENT'角色id',
privilege_id INT NOT NULL COMMENT'权限表id'
)COMMENT ='角色权限表';
INSERT INTO role_privilege (role_id,privilege_id) VALUES(1,1);
INSERT INTO role_privilege (role_id,privilege_id) VALUES(1,2);
INSERT INTO role_privilege (role_id,privilege_id) VALUES(2,3);
因为该项目只是为了验证shiro的一些功能,不包括权限配置部分,所以这里直接就在sql中配置了权限。这些权限简单总结如下:
1、苍老师(用户)属于 教师 (角色)拥有 “DRINKING_WINE”-“喝酒”,“SMOKING”-“抽烟” 两项权限
2、小炮同学(用户) 属于 学生(角色) 拥有 “PLAY_GAME”-“玩游戏” 权限
使用https://start.spring.io/快速生成项目
将项目导入Eclipse中
小红叉可以不用管的。
编写代码使该项目可以正常访问
pom.xml
<?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 https://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.3.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.xl.practice</groupId>
<artifactId>springboot-shiro-practice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-shiro-practice</name>
<description></description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>2.1.3</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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
# 配置数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springboot_shiro_practice?useUnicode=true&characterEncoding=utf8&useSSL=false&useTimezone=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 设置tomcat 端口号
server.port=8089
#Mybatis扫描
mybatis.mapper-locations=classpath*:mapping/*.xml
SpringbootShiroPracticeApplication.java
package com.xl.practice.springbootshiropractice;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.xl.practice.springbootshiropractice.Mapper")
public class SpringbootShiroPracticeApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootShiroPracticeApplication.class, args);
}
}
controller
package com.xl.practice.springbootshiropractice.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.xl.practice.springbootshiropractice.entity.User;
import com.xl.practice.springbootshiropractice.service.UserService;
@RestController
public class TestController {
@Autowired
UserService userService;
@RequestMapping("/testP")
public String testProject(String name, String password) {
User user = null;
try {
user = userService.selectUserByName(name);
} catch (Exception e) {
e.printStackTrace();
return "failed!!";
}
try {
System.out.println(user.getName() + " " +user.getPassword() +" " +user.getRemark());
} catch(Exception e) {
e.printStackTrace();
return "未查询到用户信息,请检查用户名name是否正确";
}
return "success!!! 用户名: " +user.getName() + " 密码: " +user.getPassword() +" 备注: " +user.getRemark();
}
}
entity
package com.xl.practice.springbootshiropractice.entity;
public class User {
private Integer id;
private String name;
private String password;
private String remark;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
Service
package com.xl.practice.springbootshiropractice.service;
import com.xl.practice.springbootshiropractice.entity.User;
public interface UserService {
User selectUserByName(String name);
}
ServiceImpl
package com.xl.practice.springbootshiropractice.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.xl.practice.springbootshiropractice.Mapper.UserMapper;
import com.xl.practice.springbootshiropractice.entity.User;
import com.xl.practice.springbootshiropractice.service.UserService;
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Override
public User selectUserByName(String name) {
return userMapper.selectUserByName(name);
}
}
Mapper
package com.xl.practice.springbootshiropractice.Mapper;
import com.xl.practice.springbootshiropractice.entity.User;
public interface UserMapper {
User selectUserByName(String name);
}
Mapper.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.xl.practice.springbootshiropractice.Mapper.UserMapper">
<select id="selectUserByName" parameterType="string"
resultType="com.xl.practice.springbootshiropractice.entity.User">
SELECT * FROM users WHERE name = #{name};
</select>
</mapper>
验证springboot项目是否能正常访问
如何验证?通过浏览器(客户端)传入参数name(用户名)在数据库中查找对应的用户信息User,验证是否能正常返回用户信息,如果,能,则说明项目正常,反之,则有问题,需要调试。
在浏览器中输入测试地址
查看运行结果
与预期结果一致,说明项目成功运行。到目前为止基础的项目环境已经搭建完成,接下来就需要整合Shiro,进行用户验证。
springboot整合Shiro完成用户验证
为什么需要用户验证
看下面的例子,
在TestController中添加方法:
/**
*
* @return
*/
@RequestMapping("/getSecret")
public String getSecret() {
String secretContent = "期中考试的答案";
return secretContent;
}
直接在浏览器中访问该方法:http://localhost:8089/getSecret(这个就相当于办公桌抽屉的位置,很多同学都会知道)
运行结果:
任何人都可以根据这个地址(办公桌的位置)获取到答案,本来这些答案只有老师可以获取 。
那么,怎么办呢?
在办公桌的位置专门派个管理员在那里守着,如果有人来看答案,管理员就会识别这个人是不是老师(登录用户),如果是,就给答案,反之,就拒绝。也就是说,就算学生知道了访问地址(办公桌位置)也无法获取答案,这就起到了保障作用。
这就得出第一个原因:
- 用户验证可以限制非法访问
如果答案分为高数答案和毛概答案,并且高数答案只有高数教师可以查看,毛概答案只有毛概教师可以查看。这样的话,如何限制高数教师查看毛概答案(或者是毛概教师查看高数答案呢)?那么,就需要另外一个超级管理员,这个超级管理员就负责识别来查看答案的人,不但要求是老师还要是对应学科的老师才能查看答案。也就是说,前提是要通过用户验证了的,然后再看是不是对应学科的老师(属于什么角色,拥有什么权限)才能查看对应的答案。
这就得出第二个原因:
- 用户验证是用户权限的前提条件
附:上面描述的“管理员”和“超级管理员”都可以看做是Shiro,他们所做的事情正式Shiro要做的,所有的细节Shiro都已经实现,只需要按照Shiro提供的接口进行使用就行了。
下面就来看下如何使用Shiro进行用户验证。
引入Shiro
一共三步
- 引入相关的依赖
- 编写业务Realm(继承Shiro的AuthorizingRealm),这里编写的是UserRealm。
- 编写Shiro的配置类,ShrioConfiguration
pom.xml中添加相关依赖
<!-- 引入shiro相关的依赖 -->
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.6.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.6.0</version>
</dependency>
编写UserRealm.java
package com.xl.practice.springbootshiropractice.shiro;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.codec.CodecException;
import org.apache.shiro.crypto.UnknownAlgorithmException;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import com.xl.practice.springbootshiropractice.entity.User;
import com.xl.practice.springbootshiropractice.service.UserService;
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String name = token.getPrincipal().toString();
// 获取数据库中的密码
User user = userService.selectUserByName(name);
String passwordInDB = user.getPassword();
SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(name, passwordInDB, null,
getName());
return a;
}
}
本类继承Shiro的抽象类AuthorizingRealm ,必须要重写的两个方法如上代码所示,doGetAuthorizationInfo及doGetAuthenticationInfo。其中第一个方法是用于用户授权的(这里暂不做介绍,涉及用户授权的时候会用到),第二方法doGetAuthenticationInfo用于用户验证,就是这里需要实现的。
doGetAuthenticationInfo :简单来讲就是将数据库中的用户信息(用户名及密码)查询出来,然后交给Shiro。 Shiro会自动将用户输入的用户名/密码,与从数据库中查询出来的用户名/密码进行比对,如果两者一致,则进入系统(登录成功),反之,禁止访问系统(登录失败)。另外,用户输入的密码是明文(如:666),但是数据库的密码是MD5加密了的, 所以,需要在ShiroConfiguration.java中配置密码的加密(加密方法、散列次数等)。
编写ShiroConfiguration.java
package com.xl.practice.springbootshiropractice.shiro;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfiguration {
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面。
shiroFilterFactoryBean.setLoginUrl("/notLogin");
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置映射关系
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(getUserRealm());
return securityManager;
}
@Bean
public UserRealm getUserRealm() {
UserRealm myShiroRealm = new UserRealm();
// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false;
myShiroRealm.setAuthenticationCachingEnabled(true);
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码; )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
// hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
/**
* 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
类ShiroConfiguration 简易介绍:
- 关于上面讲的用户明文密码如何与数据库中的加密密码做对比的问题。
在改类中的hashedCredentialsMatcher方法就是用于配置密码加密的,其中hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
就是用于指定加密的方法,这里即是采用“md5”。
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
这句是用于设置散列的次数,因为数据库插入密码时是散列的一次:INSERT INTO users(name,password,remark) VALUES("苍老师",MD5("111"),"你懂的"); INSERT INTO users(name,password,remark) VALUES("小炮同学",MD5("666"),"无");
所以,这里也值散列一次,默认就是一次。
- ShiroFilterFactoryBean 配置说明
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面。
shiroFilterFactoryBean.setLoginUrl("/notLogin");
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置映射关系
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
- 至少要保证有一个地址是不需要用户验证的,没错!登录地址。问题是如何设置?
filterChainDefinitionMap.put("/login", "anon");
第一个参数:“/login”,配置的是地址
第二个参数:“anon”, 配置的是Shiro的拦截器名称,也就是“anon”这个拦截器表示的是:不需要进行用户验证即可访问。
- 如何访问未登录的情况下访问地址
filterChainDefinitionMap.put("/**", "authc");
两个参数与上面相同都是分别表示地址和Shiro的拦截器名,其中“authc”表示只有通过登录后的才能访问第一个参数配置的地址。
这里第一个参数配置都是“ /** ”:表示所有的地址(但是不包括上面的地址“/login”),Shiro的这个配置得先后顺序即代表了优先级的高低。即是,假如有下面的配置:
filterChainDefinitionMap.put("/index", "anon");
filterChainDefinitionMap.put("/**", "authc");
那么,因为filterChainDefinitionMap.put("/index", "anon");
配置在filterChainDefinitionMap.put("/**", "authc");
的前面,所以,虽然后面的配置包含了“/index”地址,但是“/index”仍然可以在无需登录的情况下访问。
-
shiroFilterFactoryBean.setLoginUrl("/notLogin");
本句配置表示:如果没有登录就访问不允许未登录访问的地址,就会统一跳转到该地址 -
除了“anon”及“authc”之外Shrio还有哪些拦截器分别代表什么意思
验证
由前面的数据库可得两个用户的信息:
INSERT INTO users(name,password,remark) VALUES("苍老师",MD5("111"),"你懂的");
INSERT INTO users(name,password,remark) VALUES("小炮同学",MD5("666"),"无");
- 用户名:苍老师 , 密码:111
- 用户名:小炮同学, 密码:666
- 不用登录直接访问,直接在浏览器中输入地址:http://localhost:8089/getSecret
访问结果:显示无法进入系统的
可以回溯到前面的目录 “为什么需要用户验证” 进行对比可得,添加Shiro后达到了限制非法访问的目的。
- 使用错误的密码或用户名登录,http://localhost:8089/login?name=小炮同学&password=0000000
结果证明是无法访问的,并且验证了"/login"地址(在未登录的状态下)是可以访问的
- 使用正确的用户名/密码访问http://localhost:8089/login?name=小炮同学&password=666
登录成功。
- 登录成功后再次访问"/getSecret"看能否成功。
成功访问!所以,加上Shiro的用户验证之后,就可以限制非法访问(不登录直接通过地址访问资源)。
源码:https://github.com/michaelXu12/myrop
附:
参考文章:
SHIRO系列教材 (九)- 在 SPRINGBOOT 中整合 SHIRO —这位大佬写的是Shrio系列教程,建议都看一遍,以便对Shrio有个全面的了解