此篇文章轉載于csdn作者I_am_Rick_Hu,原文章鏈接https://blog.csdn.net/I_am_Hutengfei/article/details/100561564
前言
關于Spring Security的概念部分本文不進行贅述,本文主要針對于對Spring Security以及Springboot有一定了解的小伙伴,幫助大家使用Springboot + Spring Security 實現(xiàn)一個前后端分離登錄認證的過程。
????文章會一步一步循序漸進的帶大家敲一遍代碼。最終的代碼請看最后。
????代碼中我用到了插件lombok來生成實體的getter/setter,如果不想裝插件請自己補全getter/setter
本文主要的功能
1、前后端分離用戶登錄認證
2、基于RBAC(角色)的權限控制
文章目錄
- 1、準備工作
- 2、數(shù)據(jù)庫表設計
- 3、Spring Security核心配置:WebSecurityConfig
- 4、用戶登錄認證邏輯:UserDetailsService
- 5、用戶密碼加密
- 6、屏蔽Spring Security默認重定向登錄頁面以實現(xiàn)前后端分離功能
- 7、實現(xiàn)登錄成功/失敗、登出處理邏輯
- 8、會話管理(登錄過時、限制單用戶或多用戶登錄等)
- 9、實現(xiàn)基于JDBC的動態(tài)權限控制
- 10、結束語
文章正文
一、準備工作
1、統(tǒng)一錯誤碼枚舉
package com.springsecurity.common.response;
/**
* @version 0.1
* @Description 定義得狀態(tài)枚舉類
* @Author jin_z
* @Date 2020/3/17 22:26
*/
public enum ResultCode {
/* 成功 */
SUCCESS(200, "成功"),
/* 默認失敗 */
FAIL(999, "失敗"),
/* 參數(shù)錯誤:1000~1999 */
PARAM_NOT_VALID(1001, "參數(shù)無效"),
PARAM_IS_BLANK(1002, "參數(shù)為空"),
PARAM_TYPE_ERROR(1003, "參數(shù)類型錯誤"),
PARAM_NOT_COMPLETE(1004, "參數(shù)缺失"),
/* 用戶錯誤 */
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, "賬號下線"),
/* 業(yè)務錯誤 */
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;
}
}
2、統(tǒng)一json返回體
package com.springsecurity.common.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @version 0.1
* @Description 封裝返回得json對象
* @Author jin_z
* @Date 2020/3/17 22:42
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {
private Boolean success;
private Integer code;
private String message;
private T data;
public Result(boolean success) {
this.success = success;
this.code = success? ResultCode.SUCCESS.getCode():ResultCode.FAIL.getCode();
this.message = success? ResultCode.SUCCESS.getMessage():ResultCode.FAIL.getMessage();
}
public Result(boolean success, ResultCode resultCode) {
this.success = success;
this.code = success? ResultCode.SUCCESS.getCode(): (resultCode == null? ResultCode.FAIL.getCode(): resultCode.getCode());
this.message = success? ResultCode.SUCCESS.getMessage(): (resultCode == null? ResultCode.FAIL.getMessage(): resultCode.getMessage());
}
public Result(boolean success, T data) {
this.success = success;
this.code = success? ResultCode.SUCCESS.getCode():ResultCode.FAIL.getCode();
this.message = success? ResultCode.SUCCESS.getMessage():ResultCode.FAIL.getMessage();
this.data = data;
}
public Result(boolean success, ResultCode resultCode, T data) {
this.success = success;
this.code = success? ResultCode.SUCCESS.getCode(): (resultCode == null? ResultCode.FAIL.getCode(): resultCode.getCode());
this.message = success? ResultCode.FAIL.getMessage(): (resultCode == null? ResultCode.FAIL.getMessage(): resultCode.getMessage());
this.data = data;
}
}
3、返回體構造工具
package com.springsecurity.common.response;
/**
*@Description 封裝返回得結果方法
*@Author jin_z
*@Date 2020/5/16 18:48
*@since:
*@copyright:
*/
public class ResultTool {
public static Result success() {
return new Result(true);
}
public static <T> Result<T> success(T data) {
return new Result(true, data);
}
public static Result fail() {
return new Result(false);
}
public static Result fail(ResultCode resultEnum) {
return new Result(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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>4-springsecurity</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<properties>
<security.version>5.2.1.RELEASE</security.version>
<fastjson.version>1.2.60</fastjson.version>
<mybatisplus.version>3.1.0</mybatisplus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${security.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.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>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.2.4.RELEASE</version>
</plugin>
</plugins>
</build>
</project>
5、配置文件
spring:
application:
name: security
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/4-spring_security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=CTT
username: root
password: password
hikari:
# 最小連接數(shù)
minimum-idle: 5
# 空閑超時時間
idle-timeout: 600000
# 最大連接池大小
maximum-pool-size: 10
# 自動提交
auto-commit: true
pool-name: MyHikariCP
# 用來設置一個connection在連接池中的存活時間,默認是1800000,即30分鐘。如果設置為0,表示存活時間無限大。如果不等于0且小于30秒則會被重置回30分鐘。
max-lifetime: 1800000
# 連接超時時間
connection-timeout: 30000
# 連接測試查詢
connection-test-query: SELECT 1
server:
port: 8888
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
# 實體掃描,多個package用逗號或者分號分隔
type-aliases-package: com.springsecurity.**
global-config:
db-config:
# auto數(shù)據(jù)庫id自增
id-type: auto
# 字段策略 非空判斷
field-strategy: not_empty
configuration:
# 開啟數(shù)據(jù)庫與實體類中屬性支持駝峰模式對應
map-underscore-to-camel-case: true
cache-enabled: false
二、數(shù)據(jù)庫表設計

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(可用)',
account_non_expired tinyint(1) default 1 null comment '是否過期。默認1(沒有過期)',
account_non_locked tinyint(1) default 1 null comment '賬號是否鎖定。默認1(沒有鎖定)',
credentials_non_expired tinyint(1) default 1 null comment '證書(密碼)是否過期。默認1(沒有過期)',
create_time datetime null comment '創(chuàng)建時間',
update_time datetime null comment '修改時間',
create_user int null comment '創(chuàng)建人',
update_user int null comment '修改人'
) comment '系統(tǒng)用戶表';
create table sys_role
(
id int auto_increment primary key comment '主鍵id',
role_code varchar(32) null comment '角色碼',
role_name varchar(32) null comment '角色名',
role_description varchar(64) null comment '角色說明'
) comment '用戶角色表';
create table sys_permission(
id int auto_increment primary key comment '主鍵id',
permission_code varchar(32) null comment '權限code',
permission_name varchar(32) null comment '權限名'
) comment '權限表';
create table sys_user_role_relation(
id int auto_increment primary key comment '主鍵id',
user_id int null comment '用戶id',
role_id int null comment '角色id'
) comment '用戶角色關聯(lián)關系表';
create table sys_role_permission_relation(
id int auto_increment primary key comment '主鍵id',
role_id int null comment '角色id',
permission_id int null comment '權限id'
) comment '角色-權限關聯(lián)關系表';
create table sys_request_path(
id int auto_increment primary key comment '主鍵id',
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 '路徑權限關聯(lián)表';
-- 用戶
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, 1, 1, 1, '2019-08-29 06:28:36', '2019-09-04 20:25:36', 1, 1);
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 (2, 'user2', '用戶2', '$2a$10$uSLAeON6HWrPbPCtyqPRj.hvZfeM.tiVDZm24/gRqm4opVze1cVvC', '2019-09-05 00:07:12', 1, 1, 1, 1, '2019-08-29 06:29:24', '2019-09-05 00:07:12', 1, 2);
-- 角色
INSERT INTO sys_role (id, role_code, role_name, role_description) VALUES (1, 'admin', '管理員', '管理員,擁有所有權限');
INSERT INTO sys_role (id, role_code, role_name, role_description) VALUES (2, 'user', '普通用戶', '普通用戶,擁有部分權限');
-- 權限
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (1, 'create_user', '創(chuàng)建用戶');
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (2, 'query_user', '查看用戶');
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (3, 'delete_user', '刪除用戶');
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (4, 'modify_user', '修改用戶');
-- 請求路徑
INSERT INTO sys_request_path (id, url, description) VALUES (1, '/getUser', '查詢用戶');
-- 用戶角色關聯(lián)關系
INSERT INTO sys_user_role_relation (id, user_id, role_id) VALUES (1, 1, 1);
INSERT INTO sys_user_role_relation (id, user_id, role_id) VALUES (2, 2, 2);
-- 角色權限關聯(lián)關系
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (1, 1, 1);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (2, 1, 2);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (3, 1, 3);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (4, 1, 4);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (5, 2, 1);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (6, 2, 2);
-- 請求路徑權限關聯(lián)關系
INSERT INTO sys_request_path_permission_relation (id, url_id, permission_id) VALUES (null, 1, 2);
三、Spring Security核心配置:WebSecurityConfig
????創(chuàng)建WebSecurityConfig繼承WebSecurityConfigurerAdapter類,并實現(xiàn)configure(AuthenticationManagerBuilder auth)和 configure(HttpSecurity http)方法。后續(xù)我們會在里面加入一系列配置,包括配置認證方式、登入登出、異常處理、會話管理等。
package com.springsecurity.config;
import com.springsecurity.config.handler.*;
import com.springsecurity.config.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
* @version 0.1
* @Description webSecurity核心配置類
* @Author jin_z
* @Date 2020/4/7 22:10
*/
@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;
//實現(xiàn)權限攔截
@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);//安全元數(shù)據(jù)源
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).//同一賬號同時登錄最大用戶數(shù)
expiredSessionStrategy(sessionInformationExpiredStrategy);//會話失效(賬號被擠下線)處理邏輯
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);
}
}
四、用戶登錄認證邏輯:UserDetailsService
1、創(chuàng)建自定義UserDetailsService
????這是實現(xiàn)自定義用戶認證的核心邏輯,loadUserByUsername(String username)的參數(shù)就是登錄時提交的用戶名,返回類型是一個叫UserDetails 的接口,需要在這里構造出他的一個實現(xiàn)類User,這是Spring security提供的用戶信息實體。
package com.springsecurity.config.service;
import com.springsecurity.entity.SysPermission;
import com.springsecurity.entity.SysUser;
import com.springsecurity.service.SysPermissionService;
import com.springsecurity.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.ArrayList;
import java.util.List;
/**
* @Description TODO
* @Author jin_z
* @Date 2020/5/30 0:25
* @since:
* @copyright:
*/
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysPermissionService sysPermissionService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
if(StringUtils.isBlank(userName)) {
throw new RuntimeException("用戶不能為空");
}
//根據(jù)用戶名查詢用戶
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);
}
}
這里我們使用他的一個參數(shù)比較詳細的構造函數(shù),源碼如下
User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities)
1
其中參數(shù):
String username:用戶名
String password: 密碼
boolean enabled: 賬號是否可用
boolean accountNonExpired:賬號是否過期
boolean credentialsNonExpired:密碼是否過期
boolean accountNonLocked:賬號是否鎖定
Collection<? extends GrantedAuthority> authorities):用戶權限列表
這就與我們的創(chuàng)建的用戶表的字段對應起來了,Spring security都為我們封裝好了,如果用戶信息的狀態(tài)異常,登錄時則會拋出相應的異常,根據(jù)捕獲到的異常判斷是什么原因(賬號過期/密碼過期/賬號鎖定等等…),進而就可以提示前臺了。
????我們就按照該參數(shù)列表構造出我們所需要的數(shù)據(jù),然后返回,就完成了基于JDBC的自定義用戶認證。
????首先用戶名密碼以及用戶狀態(tài)信息都是從用戶表里進行單表查詢來的,而權限列表則是通過用戶表、角色表以及權限表等關聯(lián)查出來的,那么接下來就是準備service和dao層方法了
2、準備service和dao層方法
(1)根據(jù)用戶名查詢用戶信息
映射文件
<!--根據(jù)用戶名查詢用戶-->
<select id="selectByName" resultMap="SysUserMap">
select * from sys_user where account = #{userName};
</select>
service層
/**
* 根據(jù)用戶名查詢用戶
*
* @param userName
* @return
*/
SysUser selectByName(String userName);
(2)根據(jù)用戶名查詢用戶的權限信息
映射文件
<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);
這樣的話流程我們就理清楚了,首先根據(jù)用戶名查出對應用戶,再拿得到的用戶的用戶id去查詢它所擁有的的權限列表,最后構造出我們需要的org.springframework.security.core.userdetails.User對象。
接下來改造一下剛剛自定義的UserDetailsService
package com.springsecurity.config.service;
import com.springsecurity.entity.SysPermission;
import com.springsecurity.entity.SysUser;
import com.springsecurity.service.SysPermissionService;
import com.springsecurity.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.ArrayList;
import java.util.List;
/**
* @Description TODO
* @Author jin_z
* @Date 2020/5/30 0:25
* @since:
* @copyright:
*/
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysPermissionService sysPermissionService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
if(StringUtils.isBlank(userName)) {
throw new RuntimeException("用戶不能為空");
}
//根據(jù)用戶名查詢用戶
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的用戶認證在之前創(chuàng)建的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規(guī)定必須設置一個默認的加密方式,不允許使用明文。這個加密方式是用于在登錄時驗證密碼、注冊時需要用到。
????我們可以自己選擇一種加密方式,Spring security為我們提供了多種加密方式,我們這里使用一種強hash方式進行加密。

在WebSecurityConfig 中注入(注入即可,不用聲明使用),這樣就會對提交的密碼進行加密處理了,如果你沒有注入加密方式,運行的時候會報錯"There is no PasswordEncoder mapped for the id"錯誤。
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 設置默認的加密方式(強hash方式加密)
return new BCryptPasswordEncoder();
}
同樣的我們數(shù)據(jù)庫里存儲的密碼也要用同樣的加密方式存儲,例如我們將123456用BCryptPasswordEncoder 加密后存儲到數(shù)據(jù)庫中(注意:即使是同一個明文用這種加密方式加密出來的密文也是不同的,這就是這種加密方式的特點)

六、屏蔽Spring Security默認重定向登錄頁面以實現(xiàn)前后端分離功能
????在演示登錄之前我們先編寫一個查詢接口"/getUser",并將"/getUser"接口規(guī)定為需要擁有"query_user"權限的用戶可以訪問,并在角色-權限關聯(lián)關系表中給user1用戶所屬角色(role_id = 1)添加權限"query_user"

然后規(guī)定接口"/getUser"只能是擁有"query_user"權限的用戶可以訪問。后面我們基本都用這個查詢接口作為演示,就叫它"資源接口"吧。
http.authorizeRequests().
antMatchers("/getUser").hasAuthority("query_user").
演示登錄時,如果用戶沒有登錄去請求資源接口就會提示未登錄
????在前后端不分離的時候當用戶未登錄去訪問資源時Spring security會重定向到默認的登錄頁面,返回的是一串html標簽,這一串html標簽其實就是登錄頁面的提交表單。如圖所示

而在前后端分離的情況下(比如前臺使用VUE或JQ等)我們需要的是在前臺接收到"用戶未登錄"的提示信息,所以我們接下來要做的就是屏蔽重定向的登錄頁面,并返回統(tǒng)一的json格式的返回體。而實現(xiàn)這一功能的核心就是實現(xiàn)AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用來處理匿名用戶訪問無權限資源時的異常(即未登錄,或者登錄狀態(tài)過期失效)
package com.springsecurity.config.handler;
import com.alibaba.fastjson.JSON;
import com.springsecurity.common.response.Result;
import com.springsecurity.common.response.ResultCode;
import com.springsecurity.common.response.ResultTool;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
*@Description 匿名用戶訪問無權限資源時的異常
*@Author jin_z
*@Date 2020/5/19 23:13
*@since:
*@copyright:
*/
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Result 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、實現(xiàn)登錄成功/失敗、登出處理邏輯
????首先需要明白一件事,對于登入登出我們都不需要自己編寫controller接口,Spring Security為我們封裝好了。默認登入路徑:/login,登出路徑:/logout。當然我們可以也修改默認的名字。登錄成功失敗和登出的后續(xù)處理邏輯如何編寫會在后面慢慢解釋。
????當?shù)卿洺晒虻卿浭《夹枰祷亟y(tǒng)一的json返回體給前臺,前臺才能知道對應的做什么處理。
而實現(xiàn)登錄成功和失敗的異常處理需要分別實現(xiàn)AuthenticationSuccessHandler和AuthenticationFailureHandler接口并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中然后聲明
(1)登錄成功
package com.springsecurity.config.handler;
import com.alibaba.fastjson.JSON;
import com.springsecurity.common.response.Result;
import com.springsecurity.common.response.ResultTool;
import com.springsecurity.entity.SysUser;
import com.springsecurity.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
/**
* @version 0.1
* @Description 自定義登錄成功處理邏輯
* @Author jin_z
* @Date 2020/4/7 22:28
*/
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
SysUserService sysUserService;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
//更新用戶表上次登陸時間、更新人、更新時間等字段
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SysUser sysUser = sysUserService.selectByName(user.getUsername());
sysUser.setLastLoginTime(new Date());
sysUser.setUpdateTime(new Date());
sysUser.setUpdateUser(sysUser.getId());
sysUserService.update(sysUser);
Result result = ResultTool.success();
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
(2)登錄失敗
登錄失敗處理器主要用來對登錄失敗的場景(密碼錯誤、賬號鎖定等…)做統(tǒng)一處理并返回給前臺統(tǒng)一的json返回體。還記得我們創(chuàng)建用戶表的時候創(chuàng)建了賬號過期、密碼過期、賬號鎖定之類的字段嗎,這里就可以派上用場了.
package com.springsecurity.config.handler;
import com.alibaba.fastjson.JSON;
import com.springsecurity.common.response.Result;
import com.springsecurity.common.response.ResultCode;
import com.springsecurity.common.response.ResultTool;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
*@Description 登錄失敗處理邏輯
*@Author jin_z
*@Date 2020/5/16 18:58
*@since:
*@copyright:
*/
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//返回json數(shù)據(jù)
Result 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.FAIL);
}
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
(3)登出
同樣的登出也要將登出成功時結果返回給前臺,并且登出之后進行將cookie失效或刪除
package com.springsecurity.config.handler;
import com.alibaba.fastjson.JSON;
import com.springsecurity.common.response.Result;
import com.springsecurity.common.response.ResultTool;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
*@Description 登出成功處理邏輯
*@Author jin_z
*@Date 2020/5/19 23:36
*@since:
*@copyright:
*/
@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
Result 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、限制登錄用戶數(shù)量
比如限制同一賬號只能一個用戶使用
and().sessionManagement().
maximumSessions(1)
2、處理賬號被擠下線處理邏輯
同樣的,當賬號異地登錄導致被擠下線時也要返回給前端json格式的數(shù)據(jù),比如提示"賬號下線"、"您的賬號在異地登錄,是否是您自己操作"或者"您的賬號在異地登錄,可能由于密碼泄露,建議修改密碼"等。這時就要實現(xiàn)SessionInformationExpiredStrategy(會話信息過期策略)來自定義會話過期時的處理邏輯。
package com.springsecurity.config.handler;
import com.alibaba.fastjson.JSON;
import com.springsecurity.common.response.Result;
import com.springsecurity.common.response.ResultCode;
import com.springsecurity.common.response.ResultTool;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
*@Description 會話信息過期策略
*@Author jin_z
*@Date 2020/5/19 23:34
*@since:
*@copyright:
*/
@Component
public class CustomizeSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
Result 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(result));
}
}
3、在WebSecurityConfig中聲明
然后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后聲明,在配置同時登錄用戶數(shù)的配置下面再加一行 expiredSessionStrategy(sessionInformationExpiredStrategy)
//會話管理
and().sessionManagement().
maximumSessions(1).//同一賬號同時登錄最大用戶數(shù)
expiredSessionStrategy(sessionInformationExpiredStrategy);//會話信息過期策略會話信息過期策略(賬號被擠下線)
效果演示步驟
我電腦上用postman登錄
我電腦上請求資源接口,可以請求,如下左圖
在旁邊電腦上再登錄一次剛剛的賬號
在我電腦上再次請求資源接口,提示"賬號下線",如右下圖

九、實現(xiàn)基于JDBC的動態(tài)權限控制
在之前的章節(jié)中我們配置了一個
antMatchers("/getUser").hasAuthority("query_user")
其實我們就已經(jīng)實現(xiàn)了一個所謂的基于RBAC的權限控制,只不過我們是在WebSecurityConfig中寫死的,但是在平時開發(fā)中,難道我們每增加一個需要訪問權限控制的資源我們都要修改一下WebSecurityConfig增加一個antMatchers(…)嗎,肯定是不合理的。因此我們現(xiàn)在要做的就是將需要權限控制的資源配到數(shù)據(jù)庫中,當然也可以存儲在其他地方,比如用一個枚舉,只是我覺得存在數(shù)據(jù)庫中更加靈活一點。
????我們需要實現(xiàn)一個AccessDecisionManager(訪問決策管理器),在里面我們對當前請求的資源進行權限判斷,判斷當前登錄用戶是否擁有該權限,如果有就放行,如果沒有就拋出一個"權限不足"的異常。不過在實現(xiàn)AccessDecisionManager之前我們還需要做一件事,那就是攔截到當前的請求,并根據(jù)請求路徑從數(shù)據(jù)庫中查出當前資源路徑需要哪些權限才能訪問,然后將查出的需要的權限列表交給AccessDecisionManager去處理后續(xù)邏輯。那就是需要先實現(xiàn)一個SecurityMetadataSource,翻譯過來是"安全元數(shù)據(jù)源",我們這里使用他的一個子類FilterInvocationSecurityMetadataSource。
????在自定義的SecurityMetadataSource編寫好之后,我們還要編寫一個攔截器,增加到Spring security默認的攔截器鏈中,以達到攔截的目的。
????同樣的最后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后聲明
1、權限攔截器
package com.springsecurity.config.handler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;
import javax.servlet.*;
import java.io.IOException;
/**
*@Description 權限攔截器
*@Author jin_z
*@Date 2020/5/19 23:50
*@since:
*@copyright:
*/
@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 {
//執(zhí)行下一個攔截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
}
2、安全元數(shù)據(jù)源FilterInvocationSecurityMetadataSource
package com.springsecurity.config.handler;
import com.springsecurity.entity.SysPermission;
import com.springsecurity.service.SysPermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
/**
*@Description 實現(xiàn)權限攔截
*@Author jin_z
*@Date 2020/5/19 23:42
*@since:
*@copyright:
*/
@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){
//請求路徑?jīng)]有配置權限,表明該請求接口可以任意訪問
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
package com.springsecurity.config.handler;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Iterator;
/**
*@Description 訪問決策管理器
*@Author jin_z
*@Date 2020/5/19 23:38
*@since:
*@copyright:
*/
@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);//安全元數(shù)據(jù)源
return o;
}
});
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);//增加到默認攔截鏈中
10
十、最終的WebSecurityConfig配置
package com.springsecurity.config;
import com.springsecurity.config.handler.*;
import com.springsecurity.config.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
* @version 0.1
* @Description webSecurity核心配置類
* @Author jin_z
* @Date 2020/4/7 22:10
*/
@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;
//實現(xiàn)權限攔截
@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);//安全元數(shù)據(jù)源
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).//同一賬號同時登錄最大用戶數(shù)
expiredSessionStrategy(sessionInformationExpiredStrategy);//會話失效(賬號被擠下線)處理邏輯
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);
}
}
作者:金哲一(jinzheyi)【筆名】
本文代碼地址:https://gitee.com/jinzheyi/springboot/tree/master/springboot2.x/4-springsecurity
本文鏈接:http://www.itdecent.cn/p/172ff4e71893