springboot集成shiro+redis做认证和授权

springboot集成shiro+redis做认证和授权

  • 权限效果
  • 项目目录结构
  • 引入jar包
  • 配置文件
    • yml配置文件
    • shiro-redis.properties
    • spring-shiro.properties
    • logback.xml
  • 实体类
  • 业务层
    • 接口
    • 实现类
  • shiro配置类 BootShiroRedisAutoConfiguration
  • 自定义Realm SampleRealm
  • 异常拦截器 ShiroExceptionAdvice
  • 控制层
    • LoginController
    • TeacherController
    • StudentController
  • 前端 templates
    • login.html
    • index.html
    • 403.html
  • 测试
    • 启动redis 和 客户端
      • redis客户端
    • 启动项目浏览器访问
      • 浏览器
      • redis客户端
      • admin登入成功后点击老师主页
      • redis客户端
      • teacher登入成功后点击删除老师
      • session消失后,再点击老师主页,就会重定向到登录页面

权限效果

角色有3个,管理员,老师,学生
1.管理员可访问(老师+学生的,主页,查询,删除)
2.老师可访问(老师的:主页,查询)(学生的:主页,删除)
3.学生可访问(学生的:主页,查询)

项目目录结构

springboot集成shiro+redis做认证和授权_第1张图片

引入jar包


<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.0modelVersion>
    <groupId>org.examplegroupId>
    <artifactId>springboot_shiro_redisartifactId>
    <version>1.0-SNAPSHOTversion>
    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
        <java.version>1.8java.version>
        <shiro.version>1.8.0shiro.version>
    properties>


    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.3.RELEASEversion>
        <relativePath />
    parent>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <optional>trueoptional>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-starter-loggingartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>org.crazycakegroupId>
            <artifactId>shiro-redis-spring-boot-starterartifactId>
            <version>3.3.1version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>
        
        <dependency>
            <groupId>ch.qos.logbackgroupId>
            <artifactId>logback-classicartifactId>
        dependency>
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>jcl-over-slf4jartifactId>
        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


<configuration scan="false">
	
	<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>
			<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.logFileNamePattern>
			<cleanHistoryOnStart>truecleanHistoryOnStart>
			
			<maxHistory>7maxHistory>
			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				
				<maxFileSize>1MBmaxFileSize>
			timeBasedFileNamingAndTriggeringPolicy>
		rollingPolicy>
		<encoder>
			<pattern>[%date] [%thread] [%level] %msg%npattern>
			<charset>UTF-8charset>
		encoder>
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
			
			<level>INFOlevel>
			<onMatch>ACCEPTonMatch>
			<onMismatch>DENYonMismatch>
		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;

	/**
	 * 

@方法名描述 : Shiro过滤器

* * * anon ===> 开放路径,允许匿名访问,不需要权限和不需要登录就可以访问 * authc ===> 需要登录后才可以访问路径 * * * @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 {
    /**
     * 无权限异常 

* @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可以省略不写
    }

    /**
     * :退出接口 

* @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快速整合shirop>
            <table class="table">
                <thead>
                <tr>
                    <th>用户名th>
                    <th>密码th>
                    <th>角色th>
                tr>
                thead>
                <tbody>
                <tr>
                    <td>teachertd>
                    <td>123123td>
                    <td>老师td>
                tr>
                <tr>
                    <td>studenttd>
                    <td>123123td>
                    <td>学生td>
                tr>
                <tr>
                    <td>admintd>
                    <td>123123td>
                    <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>