Springboot+Spring Security實現前後端分離登入認證及許可權控制的示例程式碼
目錄
- 前言
- 本文主要的功能
- 一、準備工作
- 1、統一錯誤碼列舉
- 2、統一on返回體
- 3、返回體構造工具
- 4、pom
- 5、配置檔案
- 二、表設計
- 初始化表資料語句
- 三、Spring Security核心配置:WebSecurityConfig
- 四、使用者登入認證邏輯:UserDetailsService
- 1、建立自定義UserDetailsService
- 2、準備service和dao層方法
- 五、使用者密碼加密
- 六、遮蔽Spring Security預設重定向登入頁面以實現前後端分離功能
- 1、實現登入成功/失敗、登出處理邏輯
- 2、在WebSecurityConfig中的configure(HttpSecurity http)方法中宣告
- 八、會話管理(登入過時、限制單使用者或多使用者登入等)
- 1、限制登入使用者數量
- 2、處理賬號被擠下線處理邏輯
- 3、在WebSecurityConfig中宣告
- 九、實現基於JDBC的動態許可權控制
- 1、許可權攔截器
- 2、安全元資料來源FilterInvocationSecurityMetadataSource
- 3、訪問決策管理器AccessDecisionManager
- 4、在WebSecurityConfig中宣告
- 十、最終的WebSecurityConfig配置
- 十一、結束語
前言
關於Spring Security的概念部分本文不進行贅述,本文主要針對於對Spring Security以及Springboot有一定了解的小夥伴,幫助大家使用Springboot + Spring Security 實現一個前後端分離登入認證的過程。
程式碼中我用到了外掛lombok來生成實體的getter/setter,如果不想裝外掛請自己補全getter/setter
本文主要的功能
1、前後端分離使用者登入認證
2、基於RBAC(角色)的許可權控制
一、準備工作
1、統一錯誤碼列舉
/** * @Author: Hutengfei * @Description: 返回碼定義 * 規定: * #1表示成功 * #1001~1999 區間表示引數錯誤 * #2001~2999 區間表示使用者錯誤 * #3001~3999 區間表示介面異常 * @Date Create in 2019/7/22 19:28 */ public enum ResultCode { /* 成功 */ SUCCESS(200,"成功"),/* 預設失敗 */ COMMON_FAIL(999,"失敗"),/* 引數錯誤:1000~1999 */ PARAM_NOT_VALID(1001,"引數無效"),PARAM_IS_BLANK(1002,"引數為空"),PARAM_TYPE_ERROR(1003,"引數型別錯誤"),PARAM_NOT_COMPLETE(1004,"引數缺失"),/* 使用者錯誤 */ USER_NOT_LOGIN(2001,"使用者未登入"),USER_ACCOUNT_EXPIRED(2002,"賬號已過期"),USER_CREDENTIALS_ERROR(2003,"密碼錯誤"),USER_CREDENTIALS_EXPIRED(2004,"密碼過期"),USER_ACCOUNT_DISABLE(2005,"賬號不可用"),USER_ACCOUNT_LOCKED(2006,"賬號被鎖定"),USER_ACCOUNT_NOT_EXIST(2007,"賬號不存在"),USER_ACCOUNT_ALREADY_EXIST(2008,"賬號已存在"),USER_ACCOUNT_USE_BY_OTHERS(2009,"賬號下線"),/* 業務錯誤 */ NO_PERMISSION(3001,"沒有許可權"); private Integer code; private String message; ResultCode(Integer code,String message) { this.code = code; this.message = message; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } /** * 根據code獲取message * * @param code * @return */ public static String getMessageByCode(Integer code) { for (ResultCode ele : values()) { if (ele.getCode().equals(code)) { return ele.getMessage(); } } return null; } }
2、統一json返回體
/** * @Author: Hutengfei * @Description: 統一返回實體 * @Date Create in 2019/7/22 19:20 */ public class JsonResult<T> implements Serializable { private Boolean success; private Integer errorCode; private String errorMsg; private T data; public JsonResult() { } public JsonResult(boolean success) { this.success = success; this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode(); this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage(); } public JsonResult(boolean success,ResultCode resultEnum) { this.success = success; this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode()); this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage()); } public JsonResult(boolean success,T data) { this.success = success; this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode(); this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage(); this.data = data; } public JsonResult(boolean success,ResultCode resultEnum,T data) { this.success = success; this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode()); this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage()); this.data = data; } public Boolean getSuccess() { return success; } public void setSuccess(Boolean success) { this.success = success; } public Integer getErrorCode() { return errorCode; } public void setErrorCode(Integer errorCode) { this.errorCode = errorCode; } public String getErrorMsg() { return errorMsg; } public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
3、返回體構造工具
/** * @Author: Hutengfei * @Description: * @Date Create in 2019/7/22 19:52 */ public class ResultTool { public static JsonResult success() { return new JsonResult(true); } public static <T> JsonResult<T> success(T data) { return new JsonResult(true,data); } public static JsonResult fail() { return new JsonResult(false); } public static JsonResult fail(ResultCode resultEnum) { return new JsonResult(false,resultEnum); } }
4、pom
<?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.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.spring</groupId>
<artifactId>security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security</name>
<description>測試spring-security工程</description>
<properties>
<.version>1.8</java.version>
<spring.security.version>5.1.6.RELEASE</spring.security.version>
<fastjson.version>1.2.46</fastjson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- spring-security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
<!-- Hikari連線池-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<!-- 排除 tomcat-jdbc 以使用 HikariCP -->
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}<http://www.cppcns.com/version>
</dependency>
<!-- Mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatisplus-spring-boot-starter</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>2.1.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--JSON-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5、配置檔案
spring: application: name: isoftstone-security datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/spring_security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=CTT username: root password: root hikari: minimum-idle: 5 idle-timeout: 600000 maximum-pool-size: 10 auto-commit: true pool-name: MyHikariCP max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1 server: port: 8666 mybatis-plus: # 如果是放在src/main/java目錄下 classpath:/com/yourpackage/*/mapper/*Mapper.xml # 如果是放在resource目錄 classpath:/mapper/*Mapper.xml mapper-locations: classpath:mapper/*.xml,classpath:mybatis/mapping/**/*.xml #實體掃描,多個package用逗號或者分號分隔 typeAliasesPackage: com.spring.** global-config: #主鍵型別 0:"資料庫ID自增",1:"使用者輸入ID",2:"全域性唯一ID (數字型別唯一ID)",3:"全域性唯一ID UUID"; id-type: 0 #欄位策略 0:"忽略判斷",1:"非 NULL 判斷"),2:"非空判斷" field-strategy: 1 #駝峰下劃線轉換 db-column-underline: true #重新整理mapper 除錯神器 refresh-mapper: true #資料庫大寫下劃線轉換 #capital-mode: true #序列介面實現類配置,不在推薦使用此方式進行配置,請使用自定義bean注入 #key-generator: com.baomidou.mybatisplus.incrementer.H2KeyGenerator #邏輯刪除配置(下面3個配置) logic-delete-value: 0 logic-not-delete-value: 1 #自定義sql注入器,請使用自定義bean注入 #sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector #自定義填充策略介面實現,請使用自定義bean注入 # meta-object-handler: com.baomidou.springboot.MyMetaObjectHandler #自定義SQL注入器 #sql-injector: com.baomidou.springboot.xxx # SQL 解析快取,開啟後多租戶 @SqlParser 註解生效 sql-parser-cache: true configuration: map-underscore-to-camel-case: true cache-enabled: false
二、資料庫表設計
建表語句
create table sys_user ( id int auto_increment primary key,account varchar(32) not null comment '賬號',user_name varchar(32) not null comment '使用者名稱',password varchar(64) null comment '使用者密碼',last_login_time datetime null comment '上一次登入時間',enabled tinyint(1) default 1 null comment '賬號是否可用。預設為1(可用)',not_expired tinyint(1) default 1 null comment '是否過期。預設為1(沒有過期)',account_not_locked tinyint(1) default 1 null comment '賬號是否鎖定。預設為1(沒有鎖定)',credentials_not_expired tinyint(1) default 1 null comment '證書(密碼)是否過期。預設為1(沒有過期)',create_time datetime null comment '建立時間',update_time datetime null comment '修改時間',create_user int null comment '建立人',update_user int null comment '修改人' ) comment '使用者表';
create table sys_role ( id int auto_increment comment '主鍵id' primary key,role_name varchar(32) null comment '角色名',role_description varchar(64) null comment '角色說明' ) comment '使用者角色表';
create table sys_permission ( id int auto_increment comment '主鍵id' primary key,permission_code varchar(32) null comment '許可權code',permission_name varchar(32) null comment '許可權名' ) comment '許可權表';
create table sys_user_role_relation ( id int auto_increment comment '主鍵id' primary key,user_id int null comment '使用者id',role_id int null comment '角色id' ) comment '使用者角色關聯關係表';
create table sys_role_permission_relation ( id int auto_increment comment '主鍵id' primary key,role_id int null comment '角色id',permission_id int null comment '許可權id' ) comment '角色-許可權關聯關係表';
create table sys_request_path ( id int auto_increment comment '主鍵id' primary key,url varchar(64) not null comment '請求路徑',description varchar(128) null comment '路徑描述' ) comment '請求路徑';
create table sys_request_path_permission_relation ( id int null comment '主鍵id',url_id int null comment '請求路徑id',permission_id int null comment '許可權id' ) comment '路徑許可權關聯表';
初始化表資料語句
-- 使用者 INSERT INTO sys_user (id,account,user_name,password,last_login_time,enabled,account_non_expired,account_non_locked,credentials_non_expired,create_time,update_time,create_user,update_user) VALUES (1,'user1','使用者1','$2a$10$47lsFAUlWixWG17Ca3M/r.EPJVIb7Tv26ZaxhzqN65nXVcAhHQM4i','2019-09-04 20:25:36',1,'2019-08-29 06:28:36',1); INSERT INTO sys_user (id,create_pzvxMtime,update_user) VALUES (2,'user2','使用者2','$2a$10$uSLAeON6HWrPbPCtyqPRj.hvZfeM.tiVDZm24/gRqm4opVze1cVvC','2019-09-05 00:07:12','2019-08-29 06:29:24',2); -- 角色 INSERT INTO sys_role (id,role_code,role_name,role_description) VALUES (1,'admin','管理員','管理員,擁有所有許可權'); INSERT INTO sys_role (id,role_description) VALUES (2,'user','普通使用者','普通使用者,擁有部分許可權'); -- 許可權 INSERT INTO sys_permission (id,permission_code,permission_name) VALUES (1,'create_user','建立使用者'); INSERT INTO sys_permission (id,permission_name) VALUES (2,'query_user','檢視使用者'); INSERT INTO sys_permission (id,permission_name) VALUES (3,'delete_user','刪除使用者'); INSERT INTO sys_permission (id,permission_name) VALUES (4,'modify_user','修改使用者'); -- 請求路徑 INSERT INTO sys_request_path (id,url,description) VALUES (1,'/getUser','查詢使用者'); -- 使用者角色關聯關係 INSERT INTO sys_user_role_relation (id,user_id,role_id) VALUES (1,1); INSERT INTO sys_user_role_relation (id,role_id) VALUES (2,2,2); -- 角色許可權關聯關係 INSERT INTO sys_role_permission_relation (id,role_id,permission_id) VALUES (1,1); INSERT INTO sys_role_permission_relation (id,permission_id) VALUES (2,2); INSERT INTO sys_role_permission_relation (id,permission_id) VALUES (3,3); INSERT INTO sys_role_permission_relation (id,permission_id) VALUES (4,4); INSERT INTO sys_role_permission_relation (id,permission_id) VALUES (5,permission_id) VALUES (6,2); -- 請求路徑許可權關聯關係 INSERT INTO sys_request_path_permission_relation (id,url_id,permission_id) VALUES (null,2);
三、Spring Security核心配置:WebSecurityConfig
&www.cppcns.com#8194; 建立WebSecurityConfig繼承WebSecurityConfigurerAdapter類,並實現configure(AuthenticationManagerBuilder auth)和 configure(HttpSecurity http)方法。後續我們會在裡面加入一系列配置,包括配置認證方式、登入登出、異常處理、會話管理等。
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //配置認證方式等 super.configure(auth); } @Override protected void configure(HttpSecurity http) throws Exception { //http相關的配置,包括登入登出、異常處理、會話管理等 super.configure(http); } }
四、使用者登入認證邏輯:UserDetailsService
1、建立自定義UserDetailsService
這是實現自定義使用者認證的核心邏輯,loadUserByUsername(String username)的引數就是登入時提交的使用者名稱,返回型別是一個叫UserDetails 的介面,需要在這裡構造出他的一個實現類User,這是Spring security提供的使用者資訊實體。
public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //需要構造出 org.springframework.security.core.userdetails.User 物件並返回 return null; } }
這裡我們使用他的一個引數比較詳細的建構函式,原始碼如下
User(String username,String password,boolean enabled,boolean accountNonExpired,boolean credentialsNonExpired,boolean accountNonLocked,Collection<? extends GrantedAuthority> authorities)
其中引數:
String username:使用者名稱
String password: 密碼
boolean enabled: 賬號是否可用
boolean accountNonExpired:賬號是否過期
boolean credentialsNonExpired:密碼是否過期
boolean accountNonLocked:賬號是否鎖定
Collection<? extends GrantedAuthority> authorities):使用者許可權列表
這就與我們的建立的使用者表的欄位對應起來了,Spring security都為我們封裝好了,如果使用者資訊的狀態異常,登入時則會丟擲相應的異常,根據捕獲到的異常判斷是什麼原因(賬號過期/密碼過期/賬號鎖定等等…),進而就可以提示前臺了。
我們就按照該引數列表構造出我們所需要的資料,然後返回,就完成了基於JDBC的自定義使用者認證。
首先使用者名稱密碼以及使用者狀態資訊都是從使用者表裡進行單表查詢來的,而許可權列表則是通過使用者表、角色表以及許可權表等關聯查出來的,那麼接下來就是準備service和dao層方法了
2、準備service和dao層方法
(1)根據使用者名稱查詢使用者資訊
對映檔案
<!--根據使用者名稱查詢使用者--> <select id="selectByName" resultMap="SysUserMap"> select * from sys_user where account = #{userName}; </select>
service層
/** * 根據使用者名稱查詢使用者 * * @param userName * @return */ SysUser selectByName(String userName);
(2)根據使用者名稱查詢使用者的許可權資訊
對映檔案
<select id="selectListByUser" resultMap="SysPermissionMap"> SELECT p.* FROM sys_user AS u LEFT JOIN sys_user_role_relation AS ur ON u.id = ur.user_id LEFT JOIN sys_role AS r ON r.id = ur.role_id LEFT JOIN sys_role_permission_relation AS rp ON r.id = rp.role_id LEFT JOIN sys_permission AS p ON p.id = rp.permission_id WHERE u.id = #{userId} </select>
service層
/** * 查詢使用者的許可權列表 * * @param userId * @return */ List<SysPermission> selectListByUser(Integer userId);
這樣的話流程我們就理清楚了,首先根據使用者名稱查出對應使用者,再拿得到的使用者的使用者id去查詢它所擁有的的許可權列表,最後構造出我們需要的org.springframework.security.core.userdetails.User物件。
接下來改造一下剛剛自定義的UserDetailsService
public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserService sysUserService; @Autowired private SysPermissionService sysPermissionService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (username == null || "".equals(username)) { throw new RuntimeException("使用者不能為空"); } //根據使用者名稱查詢使用者 SysUser sysUser = sysUserService.selectByName(username); if (sysUser == null) { throw new RuntimeException("使用者不存在"); } List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); if (sysUser != null) { //獲取該使用者所擁有的許可權 List<SysPermission> sysPermissions = sysPermissionService.selectListByUser(sysUser.getId()); // 宣告使用者授權 sysPermissions.forEach(sysPermission -> { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(sysPermission.getPermissionCode()); grantedAuthorities.add(grantedAuthority); }); } return new User(sysUser.getAccount(),sysUser.getPassword(),sysUser.getEnabled(),sysUser.getAccountNonExpired(),sysUser.getCredentialsNonExpired(),sysUser.getAccountNonLocked(),grantedAuthorities); } }
然後將我們的自定義的基於JDBC的使用者認證在之前建立的WebSecurityConfig 中得configure(AuthenticationManagerBuilder auth)中宣告一下,到此自定義的基於JDBC的使用者認證就完成了
@Bean public UserDetailsService userDetailsService() { //獲取使用者賬號密碼及許可權資訊 return new UserDetailsServiceImpl(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //配置認證方式 auth.userDetailsService(userDetailsService()); }
五、使用者密碼加密
新版本的Spring security規定必須設定一個預設的加密方式,不允許使用明文。這個加密方式是用於在登入時驗證密碼、註冊時需要用到。
我們可以自己選擇一種加密方式,Spring security為我們提供了多種加密方式,我們這裡使用一種強hash方式進行加密。
在WebSecurityConfig 中注入(注入即可,不用宣告使用),這樣就會對提交的密碼進行加密處理了,如果你沒有注入加密方式,執行的時候會報錯"There is no PasswordEncoder mapped for the id"錯誤。
@Bean public BCryptPasswordEncoder passwordEncoder() { // 設定預設的加密方式(強hash方式加密) return new BCryptPasswordEncoder(); }
同樣的我們資料庫裡儲存的密碼也要用同樣的加密方式儲存,例如我們將123456用BCryptPasswordEncoder 加密後儲存到資料庫中(注意:即使是同一個明文用這種加密方式加密出來的密文也是不同的,這就是這種加密方式的特點)
六、遮蔽Spring Security預設重定向登入頁面以實現前後端分離功能
在演示登入之前我們先編寫一個查詢介面"/getUser",並將"/getUser"介面規定為需要擁有"query_user"許可權的使用者可以訪問,並在角色-許可權關聯關係表中給user1使用者所屬角色(role_id = 1)新增許可權"query_user"
然後規定介面"/getUser"只能是擁有"query_user"許可權的使用者可以訪問。後面我們基本都用這個查詢介面作為演示,就叫它"資源介面"吧。
http.authorizeRequests(). antMatchers("/getUser").hasAuthority("query_user").
演示登入時,如果使用者沒有登入去請求資源介面就會提示未登入
在前後端不分離的時候當用戶未登入去訪問資源時Spring security會重定向到預設的登入頁面,返回的是一串html標籤,這一串html標籤其實就是登入頁面的提交表單。如圖所示
而在前後端分離的情況下(比如前臺使用或JQ等)我們需要的是在前臺接收到"使用者未登入"的提示資訊,所以我們接下來要做的就是遮蔽重定向的登入頁面,並返回統一的json格式的返回體。而實現這一功能的核心就是實現AuthenticationEntryPoint並在WebSecurityConfig中注入,然後在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用來處理匿名使用者訪問無許可權資源時的異常(即未登入,或者登入狀態過期失效)
/** * @Author: Hutengfei * @Description: 匿名使用者訪問無許可權資源時的異常 * @Date Create in 2019/9/3 21:35 */ @Component public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,AuthenticationException e) throws IOException,ServletException { JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN); httpServletResponse.setContentType("text/json;charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }
在WebSecurityConfig中的configure(HttpSecurity http)方法中宣告
//異常處理(許可權拒絕、登入失效等) and().exceptionHandling(). authenticationEntryPoint(authenticationEntryPoint).//匿名使用者訪問無許可權資源時的異常處理
再次請求資源介面
前臺拿到這個錯誤時就可以做一些處理了,主要是退出到登入頁面。
1、實現登入成功/失敗、登出處理邏輯
首先需要明白一件事,對於登入登出我們都不需要自己編寫controller介面,Spring Security為我們封裝好了。預設登入路徑:/login,登出路徑:/logout。當然我們可以也修改預設的名字。登入成功失敗和登出的後續處理邏輯如何編寫會在後面慢慢解釋。
當登入成功或登入失敗都需要返回統一的json返回體給前臺,前臺才能知道對應的做什麼處理。
而實現登入成功和失敗的異常處理需要分別實現AuthenticationSuccessHandler和AuthenticationFailureHandler介面並在WebSecurityConfig中注入,然後在configure(HttpSecurity http)方法中然後宣告
(1)登入成功
/** * @Author: Hutengfei * @Description: 登入成功處理邏輯 * @Date Create in 2019/9/3 15:52 */ @Component public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Autowired SysUserService sysUserService; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,Authentication authentication) throws IOException,ServletException { //更新使用者表上次登入時間、更新人、更新時間等欄位 User userDetails = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); SysUser sysUser = sysUserService.selectByName(userDetails.getUsername()); sysUser.setLastLoginTime(new Date()); sysUser.setUpdateTime(new Date()); sysUser.setUpdateUser(sysUser.getId()); sysUserService.update(sysUser); //此處還可以進行一些處理,比如登入成功之後可能需要返回給前臺當前使用者有哪些選單許可權, //進而前臺動態的控制選單的顯示等,具體根據自己的業務需求進行擴充套件 //返回json資料 JsonResult result = ResultTool.success(); //處理編碼方式,防止中文亂碼的情況 httpServletResponse.setContentType("text/json;charset=utf-8"); //塞到HttpServletResponse中返回給前臺 httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }
(2)登入失敗
登入失敗處理器主要用來對登入失敗的場景(密碼錯誤、賬號鎖定等…)做統一處理並返回給前臺統一的json返回體。還記得我們建立使用者表的時候建立了賬號過期、密碼過期、賬號鎖定之類的欄位嗎,這裡就可以派上用場了.
/** * @Author: Hutengfei * @Description: 登入失敗處理邏輯 * @Date Create in 2019/9/3 15:52 */ @Component public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest,ServletException { //返回json資料 JsonResult result = null; if (e instanceof AccountExpiredException) { //賬號過期 result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED); } else if (e instanceof BadCredentialsException) { //密碼錯誤 result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR); } else if (e instanceof CredentialsExpiredException) { //密碼過期 result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED); } else if (e instanceof DisabledException) { //賬號不可用 result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE); } else if (e instanceof LockedException) { //賬號鎖定 result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED); } else if (e instanceof InternalAuthenticationServiceException) { //使用者不存在 result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST); }else{ //其他錯誤 result = ResultTool.fail(ResultCode.COMMON_FAIL); } //處理編碼方式,防止中文亂碼的情況 httpServletResponse.setContentType("text/json;charset=utf-8"); //塞到HttpServletResponse中返回給前臺 httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }
(3)登出
同樣的登出也要將登出成功時結果返回給前臺,並且登出之後進行將cookie失效或刪除
/** * @Author: Hutengfei * @Description: 登出成功處理邏輯 * @Date Create in 2019/9/4 10:17 */ @Component public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest,ServletException { JsonResult result = ResultTool.success(); httpServletResponse.setContentType("text/json;charset=utf-8"); httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }
2、在WebSecurityConfig中的configure(HttpSecurity http)方法中宣告
//登入 and().formLogin(). permitAll().//允許所有使用者 successHandler(authenticationSuccessHandler).//登入成功處理邏輯 failureHandler(authenticationFailureHandler).//登入失敗處理邏輯
//登出 and().logout(). permitAll().//允許所有使用者 logoutSuccessHandler(logoutSuccessHandler).//登出成功處理邏輯 deleteCookies("JSESSIONID").//登出之後刪除cookie
效果如圖:
登入時密碼錯誤
登入時賬號被鎖定
退出登入之後再次請求資源介面
八、會話管理(登入過時、限制單使用者或多使用者登入等)
1、限制登入使用者數量
比如限制同一賬號只能一個使用者使用
and().sessionManagement(). maximumSessions(1)
2、處理賬號被擠下線處理邏輯
同樣的,當賬號異地登入導致被擠下線時也要返回給前端json格式的資料,比如提示"賬號下線"、"您的賬號在異地登入,是否是您自己操作"或者"您的賬號在異地登入,可能由於密碼洩露,建議修改密碼"等。這時就要實現SessionInformationExpiredStrategy(會話資訊過期策略)來自定義會話過期時的處理邏輯。
/**
* @Author: Hutengfei
* @Description: 會話資訊過期策略
* @Date Create in 2019/9/4 9:34
*/
@Component
public class CustomizeSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException,ServletException {
JsonResult result = ResultTool.fail(ResultCode.USER_ACCOUNT_USE_BY_OTHERS);
HttpServletResponse httpServletResponse = sessionInformationExpiredEvent.getResponse();
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(resultwww.cppcns.com));
}
}
3、在WebSecurityConfig中宣告
然後需要在WebSecurityConfig中注入,並在configure(HttpSecurity http)方法中然後宣告,在配置同時登入使用者數的配置下面再加一行 expiredSessionStrategy(sessionInformationExpiredStrategy)
//會話管理 and().sessionManagement(). maximumSessions(1).//同一賬號同時登入最大使用者數 expiredSessionStrategy(sessionInformationExpiredStrategy);//會話資訊過期策略會話資訊過期策略(賬號被擠下線)
效果演示步驟
我電腦上用postman登入
我電腦上請求資源介面,可以請求,如下左圖
在旁邊電腦上再登入一次剛剛的賬號
在我電腦上再次請求資源介面,提示"賬號下線",如右下圖
九、實現基於JDBC的動態許可權控制
在之前的章節中我們配置了一個
antMatchers("/getUser").hasAuthority("query_user")
其實我們就已經實現了一個所謂的基於RBAC的許可權控制,只不過我們是在WebSecurityConfig中寫死的,但是在平時開發中,難道我們每增加一個需要訪問許可權控制的資源我們都要修改一下WebSecurityConfig增加一個antMatchers(…)嗎,肯定是不合理的。因此我們現在要做的就是將需要許可權控制的資源配到資料庫中,當然也可以儲存在其他地方,比如用一個列舉,只是我覺得存在資料庫中更加靈活一點。
我們需要實現一個AccessDecisionManager(訪問決策管理器),在裡面我們對當前請求的資源進行許可權判斷,判斷當前登入使用者是否擁有該許可權,如果有就放行,如果沒有就丟擲一個"許可權不足"的異常。不過在實現AccessDecisionManager之前我們還需要做一件事,那就是攔截到當前的請求,並根據請求路徑從資料庫中查出當前資源路徑需要哪些許可權才能訪問,然後將查出的需要的許可權列表交給AccessDecisionManager去處理後續邏輯。那就是需要先實現一個SecurityMetadataSource,翻譯過來是"安全元資料來源",我們這裡使用他的一個子類FilterInvocationSecurityMetadataSource。
在自定義的SecurityMetadataSource編寫好之後,我們還要編寫一個攔截器,增加到Spring security預設的攔截器鏈中,以達到攔截的目的。
同樣的最後需要在WebSecurityConfig中注入,並在configure(HttpSecurity http)方法中然後宣告
1、許可權攔截器
/** * @Author: Hutengfei * @Description: 許可權攔截器 * @Date Create in 2019/9/4 16:25 */ @Service public class CustomizeAbstractSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { @Autowired private FilterInvocationSecurityMetadataSource securityMetadataSource; @Autowired public void setMyAccessDecisionManager(CustomizeAccessDecisionManager accessDecisionManager) { super.setAccessDecisionManager(accessDecisionManager); } @Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; } @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } @Override public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse,FilterChain filterChain) throws IOException,ServletException { FilterInvocation fi = new FilterInvocation(servletRequest,servletResponse,filterChain); invoke(fi); } public void invoke(FilterInvocation fi) throws IOException,ServletException { //fi裡面有一個被攔截的url //裡面呼叫MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法獲取fi對應的所有許可權 //再呼叫MyAccessDecisionManager的decide方法來校驗使用者的許可權是否足夠 InterceptorStatusToken token = super.beforeInvocation(fi); try { //執行下一個攔截器 fi.getChain().doFilter(fi.getRequest(),fi.getResponse()); } finally { super.afterInvocation(token,null); } } }
2、安全元資料來源FilterInvocationSecurityMetadataSource
/** * @Author: Hutengfei * @Description: * @Date Create in 2019/9/3 21:06 */ @Component public class CustomizeFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { AntPathMatcher antPathMatcher = new AntPathMatcher(); @Autowired SysPermissionService sysPermissionService; @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { //獲取請求地址 String requestUrl = ((FilterInvocation) o).getRequestUrl(); //查詢具體某個介面的許可權 List<SysPermission> permissionList = sysPermissionService.selectListByPath(requestUrl); if(permissionList == null || permissionList.size() == 0){ //請求路徑沒有配置許可權,表明該請求介面可以任意訪問 return null; } String[] attributes = new String[permissionList.size()]; for(int i = 0;i<permissionList.size();i++){ attributes[i] = permissionList.get(i).getPermissionCode(); } return SecurityConfig.createList(attributes); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return true; } }
3、訪問決策管理器AccessDecisionManager
/** * @Author: Hutengfei * @Description: 訪問決策管理器 * @Date Create in 2019/9/3 20:38 */ @Component public class CustomizeAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication,Object o,Collection<ConfigAttribute> collection) throws AccessDeniedException,InsufficientAuthenticationException { Iterator<ConfigAttribute> iterator = collection.iterator(); while (iterator.hasNext()) { ConfigAttribute ca = iterator.next(); //當前請求需要的許可權 String needRole = ca.getAttribute(); //當前使用者所具有的許可權 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(needRole)) { return; } } } throw new AccessDeniedException("許可權不足!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
4、在WebSecurityConfig中宣告
先在WebSecurityConfig中注入,並在configure(HttpSecurity http)方法中然後宣告
http.authorizeRequests(). withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setAccessDecisionManager(accessDecisionManager);//訪問決策管理器 o.setSecurityMetadataSource(securityMetadataSource);//安全元資料來源 return o; } }); http.addFilterBefore(securityInterceptor,FilterSecurityInterceptor.class);//增加到預設攔截鏈中
十、最終的WebSecurityConfig配置
/** * @Author: Hutengfei * @Description: * @Date Create in 2019/8/28 20:15 */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //登入成功處理邏輯 @Autowired CustomizeAuthenticationSuccessHandler authenticationSuccessHandler; //登入失敗處理邏輯 @Autowired CustomizeAuthenticationFailureHandler authenticationFailureHandler; //許可權拒絕處理邏輯 @Autowired CustomizeAccessDeniedHandler accessDeniedHandler; //匿名使用者訪問無許可權資源時的異常 @Autowired CustomizeAuthenticationEntryPoint authenticationEntryPoint; //會話失效(賬號被擠下線)處理邏輯 @Autowired CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy; //登出成功處理邏輯 @Autowired CustomizeLogoutSuccessHandler logoutSuccessHandler; //訪問決策管理器 @Autowired CustomizeAccessDecisionManager accessDecisionManager; //實現許可權攔截 @Autowired CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource; @Autowired private CustomizeAbstractSecurityInterceptor securityInterceptor; @Bean public UserDetailsService userDetailsService() { //獲取使用者賬號密碼及許可權資訊 return new UserDetailsServiceImpl(); } @Bean public BCryptPasswordEncoder passwordEncoder() { // 設定預設的加密方式(強hash方式加密) return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(); http.authorizeRequests(). //antMatchers("/getUser").hasAuthority("query_user"). //antMatchers("/**").fullyAuthenticated(). withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setAccessDecisionManager(accessDecisionManager);//決策管理器 o.setSecurityMetadataSource(securityMetadataSource);//安全元資料來源 return o; } }). //登出 and().logout(). permitAll().//允許所有使用者 logoutSuccessHandler(logoutSuccessHandler).//登出成功處理邏輯 deleteCookies("JSESSIONID").//登出之後刪除cookie //登入 and().formLogin(). permitAll().//允許所有使用者 successHandler(authenticationSuccessHandler).//登入成功處理邏輯 failureHandler(authenticationFailureHandler).//登入失敗處理邏輯 //異常處理(許可權拒絕、登入失效等) and().exceptionHandling(). accessDeniedHandler(accessDeniedHandler).//許可權拒絕處理邏輯 authenticationEntryPoint(authenticationEntryPoint).//匿名使用者訪問無許可權資源時的異常處理 //會話管理 and().sessionManagement(). maximumSessions(1).//同一賬號同時登入最大使用者數 expiredSessionStrategy(sessionInformationExpiredStrategy);//會話失效(賬號被擠下線)處理邏輯 http.addFilterBefore(securityInterceptor,FilterSecurityInterceptor.class); } }
十一、結束語
到現在為止本文就基本結束了,在本文中我們利用Springboot+Spring security實現了前後端分離的使用者登入認證和動態的許可權訪問控制。
最後附上地址:
github
到此這篇關於Springboot+Spring Security實現前後端分離登入認證及許可權控制的示例程式碼的文章就介紹到這了,更多相關Springboot SpringSecurity前後端分離登入認證內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!