Shiro架構圖與基本知識
1、Shiro是Apache下的一個開源項目,我們稱之爲Apache Shiro。它是一個很易用與Java項目的的安全框架,提供了認證、授權、加密、會話管理,與spring Security 一樣都是做一個權限的安全框架,但是與Spring Security 相比,在於 Shiro 使用了比較簡單易懂易於使用的授權方式。shiro屬於輕量級框架,相對於security簡單的多,也沒有security那麼複雜。所以我這裏也是簡單介紹一下shiro的使用。
2、非常簡單;其基本功能點如下圖所示:
Authentication:身份認證/登錄,驗證用戶是不是擁有相應的身份;
Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限;
Session Manager:會話管理,即用戶登錄後就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;
Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲;
Web Support:Web支持,可以非常容易的集成到Web環境;
Caching:緩存,比如用戶登錄後,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率;
Concurrency:shiro支持多線程應用的併發驗證,即如在一個線程中開啓另一個線程,能把權限自動傳播過去;
Testing:提供測試支持;
Run As:允許一個用戶假裝爲另一個用戶(如果他們允許)的身份進行訪問;
Remember Me:記住我,這個是非常常見的功能,即一次登錄後,下次再來的話不用登錄了。
記住一點,Shiro不會去維護用戶、維護權限;這些需要我們自己去設計/提供;然後通過相應的接口注入給Shiro即可。
源碼地址
https://github.com/MRLEILOVE/spring-boot-shiro
數據庫結構
5張表,也就是現在流行的權限設計模型RBAC,建表SQL已放在項目中。
用戶、角色、權限、用戶-角色、角色-權限,關係如下。
使用的主要框架
- SpringBoot 2.1.6.RELEASE
- shiro-spring:1.4.0
- druid數據庫連接池:1.0.29
- mybatis-plus:3.1.1
- shiro-redis:3.1.0
項目結構
下面是整個項目結構,主要類已做註釋。
├─main
│ ├─java
│ │ └─com
│ │ └─leigq
│ │ └─www
│ │ └─shiro
│ │ │ SpringBootShiroApplication.java
│ │ │
│ │ ├─bean
│ │ │ CacheUser.java — 緩存用戶信息
│ │ │ Response.java — 統一返回結果
│ │ │
│ │ ├─config
│ │ │ DruidDataSourceConfig.java — Druid數據源配置
│ │ │ DruidMonitorConfig.java — Druid監控配置
│ │ │ MyBatisPlusConfig.java — MyBatisPlus配置
│ │ │ MySessionManager.java — 自定義session管理
│ │ │ MyShiroRealm.java — 自定義 shiroRealm, 主要是重寫其認證、授權
│ │ │ ShiroConfig.java — Shiro管理
│ │ │
│ │ ├─controller
│ │ │ LoginController.java
│ │ │ PermissionController.java
│ │ │ RoleController.java
│ │ │ RolePermissionController.java
│ │ │ UserController.java
│ │ │ UserRoleController.java
│ │ │
│ │ ├─domain
│ │ │ ├─entity
│ │ │ │ Permission.java
│ │ │ │ Role.java
│ │ │ │ RolePermission.java
│ │ │ │ User.java
│ │ │ │ UserRole.java
│ │ │ │
│ │ │ └─mapper
│ │ │ PermissionMapper.java
│ │ │ RoleMapper.java
│ │ │ RolePermissionMapper.java
│ │ │ UserMapper.java
│ │ │ UserRoleMapper.java
│ │ │
│ │ ├─service
│ │ │ │ IPermissionService.java
│ │ │ │ IRolePermissionService.java
│ │ │ │ IRoleService.java
│ │ │ │ IUserRoleService.java
│ │ │ │ IUserService.java
│ │ │ │
│ │ │ └─impl
│ │ │ PermissionServiceImpl.java
│ │ │ RolePermissionServiceImpl.java
│ │ │ RoleServiceImpl.java
│ │ │ UserRoleServiceImpl.java
│ │ │ UserServiceImpl.java
│ │ │
│ │ ├─util
│ │ │ CodeGeneratorUtils.java — MyBatisPlus代碼生成器
│ │ │
│ │ └─web
│ │ │ GlobalExceptionHand.java — 全局異常處理
│ │ │
│ │ └─exception
│ │ LoginException.java
│ │
│ └─resources
│ │ application.yml
│ │
│ ├─config
│ │ application-dev.yml
│ │ application-prod.yml
│ │ application-test.yml
│ │
│ ├─mappers
│ │ PermissionMapper.xml
│ │ RoleMapper.xml
│ │ RolePermissionMapper.xml
│ │ UserMapper.xml
│ │ UserRoleMapper.xml
│ │
│ ├─sql
│ │ shiro-V1.0.0.sql
│ │ shiro-V1.0.1.sql — 最新版SQL
│ │
│ ├─static
│ └─templates
└─test
└─java
└─com
└─leigq
└─www
└─shiro
├─base
│ BaseApplicationTests.java
│
└─test
ShiroApplicationTests.java
詳細搭建過程
建議直接將代碼拉下來對照着文檔看
1、將最新版SQL導入數據庫,SQL我已經放入項目中
2、引入依賴。
<?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.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.leigq.www</groupId>
<artifactId>spring-boot-shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-shiro</name>
<description>shiro demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<druid.version>1.0.29</druid.version>
<commons-collections4.version>4.1</commons-collections4.version>
<mybatis-plus.version>3.1.1</mybatis-plus.version>
<shiro-spring.version>1.4.0</shiro-spring.version>
<shiro-redis.version>3.1.0</shiro-redis.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- druid數據庫連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--編寫更少量的代碼:使用apache commons工具類庫:
https://www.cnblogs.com/ITtangtang/p/3966955.html-->
<!--apache.commons.lang3-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--你可以把這個工具看成是java.util的擴展-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons-collections4.version}</version>
</dependency>
<!--apache.codec:編碼方法的工具類包
https://blog.csdn.net/u012881904/article/details/52767853-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring.version}</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3、編輯application.yml
我項目中使用了多環境配置,你們可根據自己情況修改
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
use-generated-keys: true
mapper-locations: classpath*:/mappers/**/*.xml
type-aliases-package: com.leigq.www.shiro.domain.entity
server:
tomcat:
uri-encoding: UTF-8
spring:
datasource:
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKPYsCl3alwZlRb1vKoFdVu0LP3Nm/+vH5iOWxI83pkUbrQc13Lxz/VT3D+H+ziaUpUsA+ZjG4iZGTDJWZnP8kcCAwEAAQ==
driver-class-name: com.mysql.cj.jdbc.Driver
filters: config,stat,wall,slf4j
initialSize: 5
maxActive: 20
maxPoolPreparedStatementPerConnectionSize: 20
maxWait: 60000
minEvictableIdleTimeMillis: 300000
minIdle: 5
password: kGJF6c+pzVsf49LGs01ss0yijBGXIpNEp20cMkNCQo3ONaeMNPeoW9M89v+nGeiWs95/D2Ms59uGyydDGUWpmg==
poolPreparedStatements: true
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
timeBetweenEvictionRunsMillis: 60000
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=true&serverTimezone=UTC
username: root
validationQuery: SELECT 1 FROM DUAL
thymeleaf:
cache: false
redis:
host: localhost
port: 6379
timeout: 2000s
password: 111111
4、創建MySessionManager
package com.leigq.www.shiro.config;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 自定義session管理
* <br/>
* 傳統結構項目中,shiro從cookie中讀取sessionId以此來維持會話,在前後端分離的項目中(也可在移動APP項目使用),
* 我們選擇在ajax的請求頭中傳遞sessionId,因此需要重寫shiro獲取sessionId的方式。
* 自定義MySessionManager類繼承DefaultWebSessionManager類,重寫getSessionId方法
* @author :leigq
* @date :2019/7/1 10:52
*/
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果請求頭中有 Authorization 則其值爲sessionId
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
//否則按默認規則從cookie取sessionId
return super.getSessionId(request, response);
}
}
}
5、創建MyShiroRealm
package com.leigq.www.shiro.config;
import com.leigq.www.shiro.domain.entity.Permission;
import com.leigq.www.shiro.domain.entity.Role;
import com.leigq.www.shiro.domain.entity.User;
import com.leigq.www.shiro.service.IPermissionService;
import com.leigq.www.shiro.service.IRoleService;
import com.leigq.www.shiro.service.IUserService;
import lombok.extern.slf4j.Slf4j;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
/**
* @author :leigq
* @date :2019/6/28 16:31
* @description:自定義 shiroRealm, 主要是重寫其認證、授權
*/
@Slf4j
public class MyShiroRealm extends AuthorizingRealm {
@Resource
private IUserService iUserService;
@Resource
private IRoleService iRoleService;
@Resource
private IPermissionService iPermissionService;
/**
* create by: leigq
* description: 授權
* create time: 2019/7/1 10:32
*
* @return 權限信息,包括角色以及權限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.warn("開始執行授權操作.......");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//如果身份認證的時候沒有傳入User對象,這裏只能取到userName
//也就是SimpleAuthenticationInfo構造的時候第一個參數傳遞需要User對象
User user = (User) principals.getPrimaryPrincipal();
// 查詢用戶角色,一個用戶可能有多個角色
List<Role> roles = iRoleService.getUserRoles(user.getUserId());
for (Role role : roles) {
authorizationInfo.addRole(role.getRole());
// 根據角色查詢權限
List<Permission> permissions = iPermissionService.getRolePermissions(role.getRoleId());
for (Permission p : permissions) {
authorizationInfo.addStringPermission(p.getPermission());
}
}
return authorizationInfo;
}
/**
* create by: leigq
* description: 主要是用來進行身份認證的,也就是說驗證用戶輸入的賬號和密碼是否正確。
* create time: 2019/7/1 09:04
*
* @return 身份驗證信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.warn("開始進行身份認證......");
//獲取用戶的輸入的賬號.
String userName = (String) token.getPrincipal();
//通過username從數據庫中查找 User對象.
//實際項目中,這裏可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
User user = iUserService.findByUsername(userName);
if (Objects.isNull(user)) {
return null;
}
return new SimpleAuthenticationInfo(
// 這裏傳入的是user對象,比對的是用戶名,直接傳入用戶名也沒錯,但是在授權部分就需要自己重新從數據庫裏取權限
user,
// 密碼
user.getPassword(),
// salt = username + salt
ByteSource.Util.bytes(user.getCredentialsSalt()),
// realm name
getName()
);
}
}
6、創建ShiroConfig
package com.leigq.www.shiro.config;
import lombok.Data;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author :leigq
* @date :2019/6/28 16:53
* @description:shiro配置
*/
@Configuration
@ConfigurationProperties(
prefix = "spring.redis"
)
@Data
public class ShiroConfig {
private String host = "localhost";
private int port = 6379;
private String password;
private Duration timeout;
/**
* Filter工廠,設置對應的過濾條件和跳轉條件
* create by: leigq
* create time: 2019/7/3 14:29
*
* @return ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 過濾器鏈定義映射
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
/*
* anon:所有url都都可以匿名訪問,authc:所有url都必須認證通過纔可以訪問;
* 過濾鏈定義,從上向下順序執行,authc 應放在 anon 下面
* */
filterChainDefinitionMap.put("/login", "anon");
// 配置不會被攔截的鏈接 順序判斷,因爲前端模板採用了thymeleaf,這裏不能直接使用 ("/static/**", "anon")來配置匿名訪問,必須配置到每個靜態目錄
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/html/**", "anon");
// 所有url都必須認證通過纔可以訪問
filterChainDefinitionMap.put("/**", "authc");
// 配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了, 位置放在 anon、authc下面
filterChainDefinitionMap.put("/logout", "logout");
// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
// 配器shirot認登錄累面地址,前後端分離中登錄累面跳轉應由前端路由控制,後臺僅返回json數據, 對應LoginController中unauth請求
shiroFilterFactoryBean.setLoginUrl("/un_auth");
// 登錄成功後要跳轉的鏈接, 此項目是前後端分離,故此行註釋掉,登錄成功之後返回用戶基本信息及token給前端
// shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授權界面, 對應LoginController中 unauthorized 請求
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 憑證匹配器(由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了)
* create by: leigq
* create time: 2019/7/3 14:30
*
* @return HashedCredentialsMatcher
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:這裏使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次數,比如散列兩次,相當於 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 將自己的驗證方式加入容器
* create by: leigq
* create time: 2019/7/3 14:30
*
* @return MyShiroRealm
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* RedisSessionDAO shiro sessionDao層的實現 通過redis, 使用的是shiro-redis開源插件
* create by: leigq
* create time: 2019/7/3 14:30
*
* @return RedisSessionDAO
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setExpire(1800);
return redisSessionDAO;
}
/**
* Session ID 生成器
* <br/>
* create by: leigq
* <br/>
* create time: 2019/7/3 16:08
*
* @return JavaUuidSessionIdGenerator
*/
@Bean
public JavaUuidSessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}
/**
* 自定義sessionManager
* create by: leigq
* create time: 2019/7/3 14:31
*
* @return SessionManager
*/
@Bean
public SessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setSessionDAO(redisSessionDAO());
return mySessionManager;
}
/**
* 配置shiro redisManager, 使用的是shiro-redis開源插件
* <br/>
* create by: leigq
* <br/>
* create time: 2019/7/3 14:33
*
* @return RedisManager
*/
private RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setTimeout((int) timeout.toMillis());
redisManager.setPassword(password);
return redisManager;
}
/**
* cacheManager 緩存 redis實現, 使用的是shiro-redis開源插件
* <br/>
* create by: leigq
* <br/>
* create time: 2019/7/3 14:33
*
* @return RedisCacheManager
*/
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
// 必須要設置主鍵名稱,shiro-redis 插件用過這個緩存用戶信息
redisCacheManager.setPrincipalIdFieldName("userId");
return redisCacheManager;
}
/**
* create by: leigq
* description: 權限管理,配置主要是Realm的管理認證
* create time: 2019/7/1 10:09
*
* @return SecurityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
// 自定義session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定義緩存實現 使用redis
securityManager.setCacheManager(cacheManager());
return securityManager;
}
/*
* 開啓Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證
* 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public SimpleCookie cookie() {
// cookie的name,對應的默認是 JSESSIONID
SimpleCookie cookie = new SimpleCookie("SHARE_JSESSIONID");
cookie.setHttpOnly(true);
// path爲 / 用於多個系統共享 JSESSIONID
cookie.setPath("/");
return cookie;
}
/* 此項目使用 shiro 場景爲前後端分離項目,這裏先註釋掉,統一異常處理已在 GlobalExceptionHand.java 中實現 */
/**
* create by: leigq
* description: 異常處理, 詳見:https://www.cnblogs.com/libra0920/p/6289848.html
* create time: 2019/7/1 10:28
* @return SimpleMappingExceptionResolver
*/
// @Bean(name = "simpleMappingExceptionResolver")
// public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
// SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
// Properties mappings = new Properties();
// mappings.setProperty("DatabaseException", "databaseError");//數據庫異常處理
// mappings.setProperty("UnauthorizedException", "/user/403");
// r.setExceptionMappings(mappings); // None by default
// r.setDefaultErrorView("error"); // No default
// r.setExceptionAttribute("exception"); // Default is "exception"
// //r.setWarnLogCategory("example.MvcLogger"); // No default
// return r;
// }
}
7、創建LoginController
package com.leigq.www.shiro.controller;
import com.leigq.www.shiro.bean.CacheUser;
import com.leigq.www.shiro.bean.Response;
import com.leigq.www.shiro.domain.entity.User;
import com.leigq.www.shiro.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author :leigq
* @date :2019/6/28 16:55
* @description:登錄Controller
*/
@Slf4j
@RestController
public class LoginController {
@Resource
private IUserService iUserService;
@Resource
private Response response;
/**
* create by: leigq
* description: 登錄
* create time: 2019/6/28 17:11
*
* @return 登錄結果
*/
@PostMapping("/login")
public Response login(User user) {
log.warn("進入登錄.....");
String userName = user.getUserName();
String password = user.getPassword();
if (StringUtils.isBlank(userName)) {
return response.failure("用戶名爲空!");
}
if (StringUtils.isBlank(password)) {
return response.failure("密碼爲空!");
}
CacheUser loginUser = iUserService.login(userName, password);
// 登錄成功返回用戶信息
return response.success("登錄成功!", loginUser);
}
/**
* create by: leigq
* description: 登出
* create time: 2019/6/28 17:37
*/
@GetMapping("/logout")
public Response logOut() {
iUserService.logout();
return response.success("登出成功!");
}
/**
* 未登錄,shiro應重定向到登錄界面,此處返回未登錄狀態信息由前端控制跳轉頁面
* <br/>
* create by: leigq
* <br/>
* create time: 2019/7/3 14:53
* @return
*/
@RequestMapping("/un_auth")
public Response unAuth() {
return response.failure(HttpStatus.UNAUTHORIZED, "用戶未登錄!", null);
}
/**
* 未授權,無權限,此處返回未授權狀態信息由前端控制跳轉頁面
* <br/>
* create by: leigq
* <br/>
* create time: 2019/7/3 14:53
* @return
*/
@RequestMapping("/unauthorized")
public Response unauthorized() {
return response.failure(HttpStatus.FORBIDDEN, "用戶無權限!", null);
}
}
8、具體登錄方法
package com.leigq.www.shiro.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.leigq.www.shiro.bean.CacheUser;
import com.leigq.www.shiro.domain.entity.User;
import com.leigq.www.shiro.domain.mapper.UserMapper;
import com.leigq.www.shiro.service.IUserService;
import com.leigq.www.shiro.web.exception.LoginException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 服務實現類
* </p>
*
* @author leigq
* @since 2019-06-28
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public User findByUsername(String username) {
return baseMapper.selectOne(
new LambdaQueryWrapper<User>().eq(User::getUserName, username)
);
}
@Override
public CacheUser login(String userName, String password) {
// 獲取Subject實例對象,用戶實例
Subject currentUser = SecurityUtils.getSubject();
// 將用戶名和密碼封裝到UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
CacheUser cacheUser;
// 4、認證
try {
// 傳到 MyShiroRealm 類中的方法進行認證
currentUser.login(token);
// 構建緩存用戶信息返回給前端
User user = (User) currentUser.getPrincipals().getPrimaryPrincipal();
cacheUser = CacheUser.builder()
.token(currentUser.getSession().getId().toString())
.build();
BeanUtils.copyProperties(user, cacheUser);
log.warn("CacheUser is {}", cacheUser.toString());
} catch (UnknownAccountException e) {
log.error("賬戶不存在異常:", e);
throw new LoginException("賬號不存在!", e);
} catch (IncorrectCredentialsException e) {
log.error("憑據錯誤(密碼錯誤)異常:", e);
throw new LoginException("密碼不正確!", e);
} catch (AuthenticationException e) {
log.error("身份驗證異常:", e);
throw new LoginException("用戶驗證失敗!", e);
}
return cacheUser;
}
@Override
public void logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
}
@Override
public List<User> listUsers() {
return baseMapper.selectList(new LambdaQueryWrapper<>());
}
}
上面我列出了項目中主要的幾個類,大家可以對照着項目看,每個類中的註釋已經寫的很詳細了。
使用及測試
我們配置每個接口的權限使用@RequiresPermissions("user:view")
註解即可,其中user:view
對應權限表中的權限。
1、登錄測試
登錄成功會將用戶信息存入緩存。
2、請求查詢用戶接口
我們先輸入錯誤的token試試
我們再輸入正確的token試試
3、請求用戶刪除接口
因爲我們沒有給此用戶配置此權限,所以返回無權限
4、退出登錄
我們再請求用戶列表接口