springboot集成shiro+redis做认证和授权
权限效果
角色有3个,管理员,老师,学生
1.管理员可访问(老师+学生的,主页,查询,删除)
2.老师可访问(老师的:主页,查询)(学生的:主页,删除)
3.学生可访问(学生的:主页,查询)
项目目录结构
引入jar包
<?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>
<groupId>org.example</groupId>
<artifactId>springboot_shiro_redis</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
<shiro.version>1.8.0</shiro.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath />
</parent>
<dependencies>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- spring 容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
</dependencies>
</project>
配置文件
yml配置文件
server:
port: 8088
spring:
thymeleaf:
cache: false
shiro-redis.properties
#--------------------shiro-redis配置------------------------
#redis地址
shiro-redis.redis-manager.host=127.0.0.1:6379
#redis密码
#shiro-redis.redis-manager.password=12345
#用户信息存入redis第几个库
shiro-redis.redis-manager.database=5
#实体类id,默认是id,找不到id则会报错
shiro-redis.cache-manager.principal-id-field-name=userId
#自定义redis关键字前缀的会话管理
shiro-redis.session-dao.key-prefix=token:user-session:
#自定义redis关键字前缀缓存管理
shiro-redis.cache-manager.key-prefix=token:authorization:
#--------------------shiro-redis配置------------------------
spring-shiro.properties
#---------shiro配置--------------------------------------
#登录页面
shiro.loginUrl=/toLogin.htm
#无权限访问
shiro.unauthorizedUrl=/error
shiro.userNativeSessionManager=true
#禁用URL会话重写
shiro.sessionManager.sessionIdUrlRewritingEnabled=false
#自定义cookie名字,默认JSESSIONID
shiro.sessionManager.cookie.name=Authorization
#---------shiro配置--------------------------------------
logback.xml
<?xml version="1.0" encoding="utf-8"?>
<configuration scan="false">
<!--value="%d{HH:mm:ss.SSS} [%thread] %-5level %caller{3} %c: %L - %msg%n" -->
<property name="format" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %c %M: %n %replace(%caller{1}){'\t|Caller.{1}0| at|\r\n', ''} : %msg %n" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoder 默认配置为PatternLayoutEncoder -->
<encoder>
<pattern>${format}</pattern>
</encoder>
</appender>
<!-- 设置日志(访问日志,系统日志)输出位置以及格式 -->
<appender name="INFOLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/DEBUGLOG/${HOSTNAME}_%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
<!-- 根据日志文件按天回滚,保存时间为7天,7天之前的都将被清理掉 -->
<maxHistory>7</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 每个日志文件保存最大值 -->
<maxFileSize>1MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>[%date] [%thread] [%level] %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 只打印INFO日志 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<logger name="cn.itgsvip" level="info" >
<appender-ref ref="INFOLOG" />
</logger>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
实体类
package com.demo.vo;
import lombok.Data;
import java.io.Serializable;
/**
* @author 作者:wl
*/
@Data
public class UserInfoVO implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;//用户id
private String userName;//用户名
private String password;//用户密码
}
业务层
接口
package com.demo.service;
import com.demo.vo.UserInfoVO;
/**
* @author 作者:wl
*/
public interface UserInfoService {
//login登录
UserInfoVO login(String username, String password);
}
实现类
package com.demo.service.impl;
import org.springframework.stereotype.Service;
import com.demo.service.UserInfoService;
import com.demo.vo.UserInfoVO;
/**
* @author 作者:wl
*/
@Service
public class UserInfoServiceImpl implements UserInfoService {
//注入DAO实现数据库查询
@Override
public UserInfoVO login(String username, String password) {
UserInfoVO userInfo = new UserInfoVO();
/**模拟数据库账号,老师角色 **/
if("teacher".equals(username)) {
userInfo.setUserId("10001");
userInfo.setUserName(username);
userInfo.setPassword(password);
}
/**模拟数据库账号,学生角色 **/
if("student".equals(username)) {
userInfo.setUserId("10002");
userInfo.setUserName(username);
userInfo.setPassword(password);
}
/**模拟数据库账号,管理员 **/
if("admin".equals(username)) {
userInfo.setUserId("10003");
userInfo.setUserName(username);
userInfo.setPassword(password);
}
return userInfo;
}
}
shiro配置类 BootShiroRedisAutoConfiguration
package com.demo.config;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*
@Configuration用于定义配置类,可替换xml配置文件,
被注解的类内部包含有一个或多个被@Bean注解的方法,
这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,
并用于构建bean定义,初始化Spring容器。
*/
@Configuration
public class BootShiroRedisAutoConfiguration extends ShiroWebAutoConfiguration {
@Autowired
RedisSessionDAO redisSessionDAO;
/**
* <p>@方法名描述 : Shiro过滤器 </p>
* <per>
* <code>
* anon ===> 开放路径,允许匿名访问,不需要权限和不需要登录就可以访问
* authc ===> 需要登录后才可以访问路径
* </code>
* </per>
* @return
*/
@Bean("shiroFilterChainDefinition")
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/api/login.htm", "anon");
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
@Bean("authorizer")
public SampleRealm sampleRealm() {//把自定义Realm交给spring容器
return new SampleRealm();
}
@Override
protected SessionManager sessionManager() {
if (useNativeSessionManager) {
DefaultWebSessionManager nativeSessionManager = (DefaultWebSessionManager) nativeSessionManager();
//Redis Expire 命令用于设置 key 的过期时间,key 过期后将不再可用。单位以秒计。
//设置redissession失效时间,秒单位
redisSessionDAO.setExpire(60);
nativeSessionManager.setSessionDAO(redisSessionDAO);
return nativeSessionManager;
}
return new ServletContainerSessionManager();
}
}
自定义Realm SampleRealm
package com.demo.config;
import java.util.HashSet;
import java.util.Set;
import com.demo.service.UserInfoService;
import com.demo.vo.UserInfoVO;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
public class SampleRealm extends AuthorizingRealm {
@Autowired
private com.demo.service.UserInfoService UserInfoService;
//授权管理
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//principals 转成UserInfoVO对象
UserInfoVO user = (UserInfoVO) principals.getPrimaryPrincipal();
//利用SimpleAuthorizationInfo对象做授权
SimpleAuthorizationInfo sai = new SimpleAuthorizationInfo();
/** 模拟老师角色 **/
if("teacher".equals(user.getUserName())) {
System.out.println("teacher");
//授予对应角色的权限
sai.addRole("teacher");
//可以加集合权限
Set<String> permissions = new HashSet<String>();
permissions.add("teacher:find");
permissions.add("student:del");//删学生
sai.addStringPermissions(permissions);
}
/** 模拟学生角色 **/
if("student".equals(user.getUserName())) {
System.out.println("student");
//授予对应角色的权限
sai.addRole("student");
//可以加集合权限
Set<String> permissions = new HashSet<String>();
permissions.add("student:find");
sai.addStringPermissions(permissions);
}
/** 模拟管理员角色 **/
if("admin".equals(user.getUserName())) {
System.out.println("admin");
//授予对应角色的权限
sai.addRole("teacher");
sai.addRole("student");
//可以加集合权限
Set<String> permissions = new HashSet<String>();
permissions.add("teacher:find");//查老师
permissions.add("teacher:del");//删老师
permissions.add("student:find");//查学生
permissions.add("student:del");//删学生
sai.addStringPermissions(permissions);
}
return sai;
}
//认证管理
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
//取出controller的login方法里面根据username和password生成的token
UsernamePasswordToken token = (UsernamePasswordToken) authToken;
String username = token.getUsername();
String password = String.valueOf(token.getPassword());
//以此来做认证(调用业务来做认证)
//从数据库中取出账号密码
UserInfoVO userInfoVO = UserInfoService.login(username, password);
return new SimpleAuthenticationInfo(userInfoVO,password,getName());
}
}
异常拦截器 ShiroExceptionAdvice
package com.demo.config;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* @Auther:wl
*/
@ControllerAdvice
public class ShiroExceptionAdvice {
/**
* 无权限异常 </p>
* @param e AuthorizationException异常
* @return 403无权限页面
*/
@ExceptionHandler(AuthorizationException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public Object handleException(AuthorizationException e, Model model) {
System.out.println("AuthorizationException================>" + e.getMessage());
model.addAttribute("code", HttpStatus.FORBIDDEN.value());
return "403";
}
}
控制层
LoginController
package com.demo.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author 作者:wl
* 需求:
* 不同账号登录,根据权限分配,可以访问不同的页面
* 如果越权访问,就显示无权访问.....
*/
@Controller
public class LoginController {
//跳转登录页面
@RequestMapping("toLogin.htm")
public String toLogin() {
return "login";
}
//shiro认证登录接口
@RequestMapping("api/login.htm")
public String apiLogin(String username, String password) {
/**
* 把账号密码交给shiro管理
* 以后可以根据账号密码来做认证和授权
* UsernamePasswordToken
* 可以基于账号密码生成一个token
* shiro可以利用token对subject对象进行认证和授权
*
*/
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
/** 登录,交给 shiro 处理,实际登录处理逻辑还是得自己写,处理了个寂寞 **/
SecurityUtils.getSubject().login(token);
return "index";//return "redirect:/index"; redirect可以省略不写
}
/**
* :退出接口 </p>
* @return 转发到登录页面
*/
@RequestMapping("api/logout")
public String logout() {
/** 退出,交给Shiro处理 **/
SecurityUtils.getSubject().logout();
return "redirect:/login";
}
}
TeacherController
package com.demo.controller;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 老师的Controller
* 1.管理员和老师可以访问主页
* 2.管理员和老师可以访问查询页
* 3.管理员可以访问删除
*/
@RestController
@RequestMapping("teacher")
public class TeacherController {
/**
* teacher角色可以访问的路径
* @return
*/
//属于 teacher 角色
@RequiresRoles("teacher")
@RequestMapping
public String adminIndex() {
return "老师的主页权限";
}
/**
* teacher:find权限可以访问的路径
* @return
*/
//符合teacher:find权限要求
@RequiresPermissions("teacher:find")
@RequestMapping("find")
public String adminFind() {
return "老师的查询权限";
}
/**
* 拥有 teacher和admin角色 和 teacher:del权限可以访问的路径
* @return
*/
//属于 teacher 或者 admin 之一;修改logical为OR 即可
@RequiresRoles(logical = Logical.OR, value = { "teacher", "admin" })
//符合teacher:del 权限要求
@RequiresPermissions("teacher:del")
@RequestMapping("del")
public String adminDel() {
return "老师的删除权限";
}
}
StudentController
package com.demo.controller;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 学生的Controller
* 1.管理员和学生可以访问主页
* 2.管理员和学生可以访问查询页
* 3.管理员和老师可以访问删除
*/
@RestController
@RequestMapping("student")
public class StudentController {
/**
* teacher角色可以访问的路径
* @return
*/
@RequiresRoles("student")
@RequestMapping
public String adminIndex() {
return "学生的主页权限";
}
/**
* teacher:find权限可以访问的路径
* @return
*/
@RequiresPermissions("student:find")
@RequestMapping("find")
public String adminFind() {
return "学生的查询权限";
}
/**
* 拥有 teacher和admin角色 和 teacher:del权限可以访问的路径
* @return
*/
@RequiresRoles(logical = Logical.OR, value = { "teacher", "admin" })
@RequiresPermissions("student:del")
@RequestMapping("del")
public String adminDel() {
return "学生的删除权限";
}
}
前端 templates
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head fragment="head">
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="viewport" content="width=device-width"/>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,300,400italic,400,600italic,600,700italic,700,800italic,800"
rel="stylesheet" type="text/css"/>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"
type="text/css"/>
<style>
body {
margin-top: 60px;
}
.box {
padding: 50px;
text-align: center;
vertical-align: middle;
}
.custom-header {
border: 2px solid #3254a0;
}
</style>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<p>springboot快速整合shiro</p>
<table class="table">
<thead>
<tr>
<th>用户名</th>
<th>密码</th>
<th>角色</th>
</tr>
</thead>
<tbody>
<tr>
<td>teacher</td>
<td>123123</td>
<td>老师</td>
</tr>
<tr>
<td>student</td>
<td>123123</td>
<td>学生</td>
</tr>
<tr>
<td>admin</td>
<td>123123</td>
<td>管理员</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">登录</h3>
</div>
<div class="panel-body">
<form name="loginform" action="api/login.htm" method="POST" accept-charset="UTF-8" role="form">
<fieldset>
<div class="form-group">
<input class="form-control" placeholder="请输入账号" name="username" type="text"/>
</div>
<div class="form-group">
<input class="form-control" placeholder="请输入密码" name="password" type="password"/>
</div>
<input class="btn btn-lg btn-success btn-block" type="submit" value="登录"/>
</fieldset>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"></script>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
</body>
</html>
index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head fragment="head">
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width" />
<link
href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,300,400italic,400,600italic,600,700italic,700,800italic,800"
rel="stylesheet" type="text/css" />
<link
href="https://netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
rel="stylesheet" type="text/css" />
<style>
<style>
body {
margin-top: 60px;
}
.box {
padding: 50px;
text-align: center;
vertical-align: middle;
}
.custom-header {
border: 2px solid #3254a0;
}
</style>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<h3>老师权限操作</h3>
<a href="/teacher">teacher:老师主页权限</a><br />
<a href="/teacher/find">find:老师查询权限</a><br />
<a href="/teacher/del">del:老师删除权限</a><br />
</div>
<div class="col-md-4 col-md-offset-4">
<h3>学生权限操作</h3>
<a href="/student">student:学生主页</a><br />
<a href="/student/find">find:学生查询权限</a><br />
<a href="/student/del">del:学生没有删除权限,只有老师和管理员有</a><br />
</div>
<div class="col-md-4 col-md-offset-4">
<a href="/toLogin.htm">登录页面</a><br />
</div>
<div class="col-md-4 col-md-offset-4">
<a href="/api/logout">退出登录页面</a><br />
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"></script>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
</body>
</html>
403.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="viewport" content="width=device-width"/>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,300,400italic,400,600italic,600,700italic,700,800italic,800"
rel="stylesheet" type="text/css"/>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"
type="text/css"/>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="box col-md-6 col-md-offset-3">
<div class="custom-header">
</div>
<div class="logo">
<h1 th:text="${code}"></h1>
</div>
<p class="lead text-muted">你的权限不足....</p>
<a href="/" class="btn btn-primary">Go Home</a>
</div>
</div>
</div>
</body>
</html>
测试
启动redis 和 客户端
redis客户端
启动项目浏览器访问
浏览器
redis客户端
可以看到redis里面存了一个key为user-session,value为浏览器request请求头里面Cookies的Authorization的值
admin登入成功后点击老师主页
redis客户端
可以看到redis里面存了一个key为authorization,value为自定义的Realm的信息
teacher登入成功后点击删除老师
同样可以看到redis里面存了一个key为authorization,value为自定义的Realm的信息