Springboot+Spring Security實(shí)現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的示例代碼
前言
關(guān)于Spring Security的概念部分本文不進(jìn)行贅述,本文主要針對于對Spring Security以及Springboot有一定了解的小伙伴,幫助大家使用Springboot + Spring Security 實(shí)現(xiàn)一個(gè)前后端分離登錄認(rèn)證的過程。
文章會一步一步循序漸進(jìn)的帶大家敲一遍代碼。最終的代碼請看最后。
代碼中我用到了插件lombok來生成實(shí)體的getter/setter,如果不想裝插件請自己補(bǔ)全getter/setter
本文主要的功能
1、前后端分離用戶登錄認(rèn)證
2、基于RBAC(角色)的權(quán)限控制
一、準(zhǔn)備工作
1、統(tǒng)一錯(cuò)誤碼枚舉
/**
* @Author: Hutengfei
* @Description: 返回碼定義
* 規(guī)定:
* #1表示成功
* #1001~1999 區(qū)間表示參數(shù)錯(cuò)誤
* #2001~2999 區(qū)間表示用戶錯(cuò)誤
* #3001~3999 區(qū)間表示接口異常
* @Date Create in 2019/7/22 19:28
*/
public enum ResultCode {
/* 成功 */
SUCCESS(200, "成功"),
/* 默認(rèn)失敗 */
COMMON_FAIL(999, "失敗"),
/* 參數(shù)錯(cuò)誤:1000~1999 */
PARAM_NOT_VALID(1001, "參數(shù)無效"),
PARAM_IS_BLANK(1002, "參數(shù)為空"),
PARAM_TYPE_ERROR(1003, "參數(shù)類型錯(cuò)誤"),
PARAM_NOT_COMPLETE(1004, "參數(shù)缺失"),
/* 用戶錯(cuò)誤 */
USER_NOT_LOGIN(2001, "用戶未登錄"),
USER_ACCOUNT_EXPIRED(2002, "賬號已過期"),
USER_CREDENTIALS_ERROR(2003, "密碼錯(cuò)誤"),
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è)務(wù)錯(cuò)誤 */
NO_PERMISSION(3001, "沒有權(quán)限");
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;
}
/**
* 根據(jù)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、統(tǒng)一json返回體
/**
* @Author: Hutengfei
* @Description: 統(tǒng)一返回實(shí)體
* @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、返回體構(gòu)造工具
/**
* @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>
<java.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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</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
#實(shí)體掃描,多個(gè)package用逗號或者分號分隔
typeAliasesPackage: com.spring.**
global-config:
#主鍵類型 0:"數(shù)據(jù)庫ID自增", 1:"用戶輸入ID",2:"全局唯一ID (數(shù)字類型唯一ID)", 3:"全局唯一ID UUID";
id-type: 0
#字段策略 0:"忽略判斷",1:"非 NULL 判斷"),2:"非空判斷"
field-strategy: 1
#駝峰下劃線轉(zhuǎn)換
db-column-underline: true
#刷新mapper 調(diào)試神器
refresh-mapper: true
#數(shù)據(jù)庫大寫下劃線轉(zhuǎn)換
#capital-mode: true
#序列接口實(shí)現(xiàn)類配置,不在推薦使用此方式進(jìn)行配置,請使用自定義bean注入
#key-generator: com.baomidou.mybatisplus.incrementer.H2KeyGenerator
#邏輯刪除配置(下面3個(gè)配置)
logic-delete-value: 0
logic-not-delete-value: 1
#自定義sql注入器,不在推薦使用此方式進(jìn)行配置,請使用自定義bean注入
#sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector
#自定義填充策略接口實(shí)現(xiàn),不在推薦使用此方式進(jìn)行配置,請使用自定義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
二、數(shù)據(jù)庫表設(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 '上一次登錄時(shí)間',
enabled tinyint(1) default 1 null comment '賬號是否可用。默認(rèn)為1(可用)',
not_expired tinyint(1) default 1 null comment '是否過期。默認(rèn)為1(沒有過期)',
account_not_locked tinyint(1) default 1 null comment '賬號是否鎖定。默認(rèn)為1(沒有鎖定)',
credentials_not_expired tinyint(1) default 1 null comment '證書(密碼)是否過期。默認(rèn)為1(沒有過期)',
create_time datetime null comment '創(chuàng)建時(shí)間',
update_time datetime null comment '修改時(shí)間',
create_user int null comment '創(chuàng)建人',
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 '權(quán)限code',
permission_name varchar(32) null comment '權(quán)限名'
)
comment '權(quán)限表';
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 '用戶角色關(guān)聯(lián)關(guān)系表';
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 '權(quán)限id'
)
comment '角色-權(quán)限關(guān)聯(lián)關(guān)系表';
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 '權(quán)限id'
)
comment '路徑權(quán)限關(guān)聯(lián)表';
初始化表數(shù)據(jù)語句
-- 用戶 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', '管理員', '管理員,擁有所有權(quán)限'); INSERT INTO sys_role (id, role_code, role_name, role_description) VALUES (2, 'user', '普通用戶', '普通用戶,擁有部分權(quán)限'); -- 權(quán)限 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', '查詢用戶'); -- 用戶角色關(guān)聯(lián)關(guā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); -- 角色權(quán)限關(guān)聯(lián)關(guā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); -- 請求路徑權(quán)限關(guān)聯(lián)關(guān)系 INSERT INTO sys_request_path_permission_relation (id, url_id, permission_id) VALUES (null, 1, 2);
三、Spring Security核心配置:WebSecurityConfig
創(chuàng)建WebSecurityConfig繼承WebSecurityConfigurerAdapter類,并實(shí)現(xiàn)configure(AuthenticationManagerBuilder auth)和 configure(HttpSecurity http)方法。后續(xù)我們會在里面加入一系列配置,包括配置認(rèn)證方式、登入登出、異常處理、會話管理等。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置認(rèn)證方式等
super.configure(auth);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//http相關(guān)的配置,包括登入登出、異常處理、會話管理等
super.configure(http);
}
}
四、用戶登錄認(rèn)證邏輯:UserDetailsService
1、創(chuàng)建自定義UserDetailsService
這是實(shí)現(xiàn)自定義用戶認(rèn)證的核心邏輯,loadUserByUsername(String username)的參數(shù)就是登錄時(shí)提交的用戶名,返回類型是一個(gè)叫UserDetails 的接口,需要在這里構(gòu)造出他的一個(gè)實(shí)現(xiàn)類User,這是Spring security提供的用戶信息實(shí)體。
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//需要構(gòu)造出 org.springframework.security.core.userdetails.User 對象并返回
return null;
}
}
這里我們使用他的一個(gè)參數(shù)比較詳細(xì)的構(gòu)造函數(shù),源碼如下
User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities)
其中參數(shù):
String username:用戶名
String password: 密碼
boolean enabled: 賬號是否可用
boolean accountNonExpired:賬號是否過期
boolean credentialsNonExpired:密碼是否過期
boolean accountNonLocked:賬號是否鎖定
Collection<? extends GrantedAuthority> authorities):用戶權(quán)限列表
這就與我們的創(chuàng)建的用戶表的字段對應(yīng)起來了,Spring security都為我們封裝好了,如果用戶信息的狀態(tài)異常,登錄時(shí)則會拋出相應(yīng)的異常,根據(jù)捕獲到的異常判斷是什么原因(賬號過期/密碼過期/賬號鎖定等等…),進(jìn)而就可以提示前臺了。
我們就按照該參數(shù)列表構(gòu)造出我們所需要的數(shù)據(jù),然后返回,就完成了基于JDBC的自定義用戶認(rèn)證。
首先用戶名密碼以及用戶狀態(tài)信息都是從用戶表里進(jìn)行單表查詢來的,而權(quán)限列表則是通過用戶表、角色表以及權(quán)限表等關(guān)聯(lián)查出來的,那么接下來就是準(zhǔn)備service和dao層方法了
2、準(zhǔn)備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ù)用戶名查詢用戶的權(quán)限信息
映射文件
<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層
/**
* 查詢用戶的權(quán)限列表
*
* @param userId
* @return
*/
List<SysPermission> selectListByUser(Integer userId);
這樣的話流程我們就理清楚了,首先根據(jù)用戶名查出對應(yīng)用戶,再拿得到的用戶的用戶id去查詢它所擁有的的權(quán)限列表,最后構(gòu)造出我們需要的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("用戶不能為空");
}
//根據(jù)用戶名查詢用戶
SysUser sysUser = sysUserService.selectByName(username);
if (sysUser == null) {
throw new RuntimeException("用戶不存在");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (sysUser != null) {
//獲取該用戶所擁有的權(quán)限
List<SysPermission> sysPermissions = sysPermissionService.selectListByUser(sysUser.getId());
// 聲明用戶授權(quán)
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的用戶認(rèn)證在之前創(chuàng)建的WebSecurityConfig 中得configure(AuthenticationManagerBuilder auth)中聲明一下,到此自定義的基于JDBC的用戶認(rèn)證就完成了
@Bean
public UserDetailsService userDetailsService() {
//獲取用戶賬號密碼及權(quán)限信息
return new UserDetailsServiceImpl();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置認(rèn)證方式
auth.userDetailsService(userDetailsService());
}
五、用戶密碼加密
新版本的Spring security規(guī)定必須設(shè)置一個(gè)默認(rèn)的加密方式,不允許使用明文。這個(gè)加密方式是用于在登錄時(shí)驗(yàn)證密碼、注冊時(shí)需要用到。
我們可以自己選擇一種加密方式,Spring security為我們提供了多種加密方式,我們這里使用一種強(qiáng)hash方式進(jìn)行加密。
在WebSecurityConfig 中注入(注入即可,不用聲明使用),這樣就會對提交的密碼進(jìn)行加密處理了,如果你沒有注入加密方式,運(yùn)行的時(shí)候會報(bào)錯(cuò)"There is no PasswordEncoder mapped for the id"錯(cuò)誤。
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 設(shè)置默認(rèn)的加密方式(強(qiáng)hash方式加密)
return new BCryptPasswordEncoder();
}
同樣的我們數(shù)據(jù)庫里存儲的密碼也要用同樣的加密方式存儲,例如我們將123456用BCryptPasswordEncoder 加密后存儲到數(shù)據(jù)庫中(注意:即使是同一個(gè)明文用這種加密方式加密出來的密文也是不同的,這就是這種加密方式的特點(diǎn))

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

然后規(guī)定接口"/getUser"只能是擁有"query_user"權(quán)限的用戶可以訪問。后面我們基本都用這個(gè)查詢接口作為演示,就叫它"資源接口"吧。
http.authorizeRequests().
antMatchers("/getUser").hasAuthority("query_user").
演示登錄時(shí),如果用戶沒有登錄去請求資源接口就會提示未登錄
在前后端不分離的時(shí)候當(dāng)用戶未登錄去訪問資源時(shí)Spring security會重定向到默認(rèn)的登錄頁面,返回的是一串html標(biāo)簽,這一串html標(biāo)簽其實(shí)就是登錄頁面的提交表單。如圖所示
而在前后端分離的情況下(比如前臺使用VUE或JQ等)我們需要的是在前臺接收到"用戶未登錄"的提示信息,所以我們接下來要做的就是屏蔽重定向的登錄頁面,并返回統(tǒng)一的json格式的返回體。而實(shí)現(xiàn)這一功能的核心就是實(shí)現(xiàn)AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用來處理匿名用戶訪問無權(quán)限資源時(shí)的異常(即未登錄,或者登錄狀態(tài)過期失效)
/**
* @Author: Hutengfei
* @Description: 匿名用戶訪問無權(quán)限資源時(shí)的異常
* @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)方法中聲明
//異常處理(權(quán)限拒絕、登錄失效等) and().exceptionHandling(). authenticationEntryPoint(authenticationEntryPoint).//匿名用戶訪問無權(quán)限資源時(shí)的異常處理
再次請求資源接口

前臺拿到這個(gè)錯(cuò)誤時(shí)就可以做一些處理了,主要是退出到登錄頁面。
1、實(shí)現(xiàn)登錄成功/失敗、登出處理邏輯
首先需要明白一件事,對于登入登出我們都不需要自己編寫controller接口,Spring Security為我們封裝好了。默認(rèn)登入路徑:/login,登出路徑:/logout。當(dāng)然我們可以也修改默認(rèn)的名字。登錄成功失敗和登出的后續(xù)處理邏輯如何編寫會在后面慢慢解釋。
當(dāng)?shù)卿洺晒虻卿浭《夹枰祷亟y(tǒng)一的json返回體給前臺,前臺才能知道對應(yīng)的做什么處理。
而實(shí)現(xiàn)登錄成功和失敗的異常處理需要分別實(shí)現(xiàn)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, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//更新用戶表上次登錄時(shí)間、更新人、更新時(shí)間等字段
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);
//此處還可以進(jìn)行一些處理,比如登錄成功之后可能需要返回給前臺當(dāng)前用戶有哪些菜單權(quán)限,
//進(jìn)而前臺動態(tài)的控制菜單的顯示等,具體根據(jù)自己的業(yè)務(wù)需求進(jìn)行擴(kuò)展
//返回json數(shù)據(jù)
JsonResult result = ResultTool.success();
//處理編碼方式,防止中文亂碼的情況
httpServletResponse.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回給前臺
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
(2)登錄失敗
登錄失敗處理器主要用來對登錄失敗的場景(密碼錯(cuò)誤、賬號鎖定等…)做統(tǒng)一處理并返回給前臺統(tǒng)一的json返回體。還記得我們創(chuàng)建用戶表的時(shí)候創(chuàng)建了賬號過期、密碼過期、賬號鎖定之類的字段嗎,這里就可以派上用場了.
/**
* @Author: Hutengfei
* @Description: 登錄失敗處理邏輯
* @Date Create in 2019/9/3 15:52
*/
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//返回json數(shù)據(jù)
JsonResult result = null;
if (e instanceof AccountExpiredException) {
//賬號過期
result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
} else if (e instanceof BadCredentialsException) {
//密碼錯(cuò)誤
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{
//其他錯(cuò)誤
result = ResultTool.fail(ResultCode.COMMON_FAIL);
}
//處理編碼方式,防止中文亂碼的情況
httpServletResponse.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回給前臺
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
(3)登出
同樣的登出也要將登出成功時(shí)結(jié)果返回給前臺,并且登出之后進(jìn)行將cookie失效或刪除
/**
* @Author: Hutengfei
* @Description: 登出成功處理邏輯
* @Date Create in 2019/9/4 10:17
*/
@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, 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
效果如圖:
登錄時(shí)密碼錯(cuò)誤

登錄時(shí)賬號被鎖定

退出登錄之后再次請求資源接口

八、會話管理(登錄過時(shí)、限制單用戶或多用戶登錄等)
1、限制登錄用戶數(shù)量
比如限制同一賬號只能一個(gè)用戶使用
and().sessionManagement().
maximumSessions(1)
2、處理賬號被擠下線處理邏輯
同樣的,當(dāng)賬號異地登錄導(dǎo)致被擠下線時(shí)也要返回給前端json格式的數(shù)據(jù),比如提示"賬號下線"、"您的賬號在異地登錄,是否是您自己操作"或者"您的賬號在異地登錄,可能由于密碼泄露,建議修改密碼"等。這時(shí)就要實(shí)現(xiàn)SessionInformationExpiredStrategy(會話信息過期策略)來自定義會話過期時(shí)的處理邏輯。
/**
* @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(result));
}
}
3、在WebSecurityConfig中聲明
然后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后聲明,在配置同時(shí)登錄用戶數(shù)的配置下面再加一行 expiredSessionStrategy(sessionInformationExpiredStrategy)
//會話管理 and().sessionManagement(). maximumSessions(1).//同一賬號同時(shí)登錄最大用戶數(shù) expiredSessionStrategy(sessionInformationExpiredStrategy);//會話信息過期策略會話信息過期策略(賬號被擠下線)
效果演示步驟
我電腦上用postman登錄
我電腦上請求資源接口,可以請求,如下左圖
在旁邊電腦上再登錄一次剛剛的賬號
在我電腦上再次請求資源接口,提示"賬號下線",如右下圖

九、實(shí)現(xiàn)基于JDBC的動態(tài)權(quán)限控制
在之前的章節(jié)中我們配置了一個(gè)
antMatchers("/getUser").hasAuthority("query_user")
其實(shí)我們就已經(jīng)實(shí)現(xiàn)了一個(gè)所謂的基于RBAC的權(quán)限控制,只不過我們是在WebSecurityConfig中寫死的,但是在平時(shí)開發(fā)中,難道我們每增加一個(gè)需要訪問權(quán)限控制的資源我們都要修改一下WebSecurityConfig增加一個(gè)antMatchers(…)嗎,肯定是不合理的。因此我們現(xiàn)在要做的就是將需要權(quán)限控制的資源配到數(shù)據(jù)庫中,當(dāng)然也可以存儲在其他地方,比如用一個(gè)枚舉,只是我覺得存在數(shù)據(jù)庫中更加靈活一點(diǎn)。
我們需要實(shí)現(xiàn)一個(gè)AccessDecisionManager(訪問決策管理器),在里面我們對當(dāng)前請求的資源進(jìn)行權(quán)限判斷,判斷當(dāng)前登錄用戶是否擁有該權(quán)限,如果有就放行,如果沒有就拋出一個(gè)"權(quán)限不足"的異常。不過在實(shí)現(xiàn)AccessDecisionManager之前我們還需要做一件事,那就是攔截到當(dāng)前的請求,并根據(jù)請求路徑從數(shù)據(jù)庫中查出當(dāng)前資源路徑需要哪些權(quán)限才能訪問,然后將查出的需要的權(quán)限列表交給AccessDecisionManager去處理后續(xù)邏輯。那就是需要先實(shí)現(xiàn)一個(gè)SecurityMetadataSource,翻譯過來是"安全元數(shù)據(jù)源",我們這里使用他的一個(gè)子類FilterInvocationSecurityMetadataSource。
在自定義的SecurityMetadataSource編寫好之后,我們還要編寫一個(gè)攔截器,增加到Spring security默認(rèn)的攔截器鏈中,以達(dá)到攔截的目的。
同樣的最后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后聲明
1、權(quán)限攔截器
/**
* @Author: Hutengfei
* @Description: 權(quán)限攔截器
* @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里面有一個(gè)被攔截的url
//里面調(diào)用MyInvocationSecurityMetadataSource的getAttributes(Object object)這個(gè)方法獲取fi對應(yīng)的所有權(quán)限
//再調(diào)用MyAccessDecisionManager的decide方法來校驗(yàn)用戶的權(quán)限是否足夠
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//執(zhí)行下一個(gè)攔截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
}
2、安全元數(shù)據(jù)源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();
//查詢具體某個(gè)接口的權(quán)限
List<SysPermission> permissionList = sysPermissionService.selectListByPath(requestUrl);
if(permissionList == null || permissionList.size() == 0){
//請求路徑?jīng)]有配置權(quán)限,表明該請求接口可以任意訪問
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();
//當(dāng)前請求需要的權(quán)限
String needRole = ca.getAttribute();
//當(dāng)前用戶所具有的權(quán)限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("權(quán)限不足!");
}
@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);//增加到默認(rèn)攔截鏈中
十、最終的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;
//權(quán)限拒絕處理邏輯
@Autowired
CustomizeAccessDeniedHandler accessDeniedHandler;
//匿名用戶訪問無權(quán)限資源時(shí)的異常
@Autowired
CustomizeAuthenticationEntryPoint authenticationEntryPoint;
//會話失效(賬號被擠下線)處理邏輯
@Autowired
CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy;
//登出成功處理邏輯
@Autowired
CustomizeLogoutSuccessHandler logoutSuccessHandler;
//訪問決策管理器
@Autowired
CustomizeAccessDecisionManager accessDecisionManager;
//實(shí)現(xiàn)權(quán)限攔截
@Autowired
CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
private CustomizeAbstractSecurityInterceptor securityInterceptor;
@Bean
public UserDetailsService userDetailsService() {
//獲取用戶賬號密碼及權(quán)限信息
return new UserDetailsServiceImpl();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 設(shè)置默認(rèn)的加密方式(強(qiáng)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).//登錄失敗處理邏輯
//異常處理(權(quán)限拒絕、登錄失效等)
and().exceptionHandling().
accessDeniedHandler(accessDeniedHandler).//權(quán)限拒絕處理邏輯
authenticationEntryPoint(authenticationEntryPoint).//匿名用戶訪問無權(quán)限資源時(shí)的異常處理
//會話管理
and().sessionManagement().
maximumSessions(1).//同一賬號同時(shí)登錄最大用戶數(shù)
expiredSessionStrategy(sessionInformationExpiredStrategy);//會話失效(賬號被擠下線)處理邏輯
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);
}
}
十一、結(jié)束語
到現(xiàn)在為止本文就基本結(jié)束了,在本文中我們利用Springboot+Spring security實(shí)現(xiàn)了前后端分離的用戶登錄認(rèn)證和動態(tài)的權(quán)限訪問控制。
最后附上github地址:
github
到此這篇關(guān)于Springboot+Spring Security實(shí)現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的示例代碼的文章就介紹到這了,更多相關(guān)Springboot SpringSecurity前后端分離登錄認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合Shiro實(shí)現(xiàn)登錄認(rèn)證的方法
- springboot前后端分離集成CAS單點(diǎn)登錄(統(tǒng)一認(rèn)證)
- SpringBoot+Vue+JWT的前后端分離登錄認(rèn)證詳細(xì)步驟
- SpringBoot?實(shí)現(xiàn)CAS?Server統(tǒng)一登錄認(rèn)證的詳細(xì)步驟
- Vue+Jwt+SpringBoot+Ldap完成登錄認(rèn)證的示例代碼
- 基于springboot實(shí)現(xiàn)整合shiro實(shí)現(xiàn)登錄認(rèn)證以及授權(quán)過程解析
- SpringBoot整合Sa-Token實(shí)現(xiàn)登錄認(rèn)證的示例代碼
- Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過程
- SpringBoot+MyBatis Plus實(shí)現(xiàn)用戶登錄認(rèn)證
相關(guān)文章
一文帶你理解@RefreshScope注解實(shí)現(xiàn)動態(tài)刷新原理
RefeshScope這個(gè)注解想必大家都用過,在微服務(wù)配置中心的場景下經(jīng)常出現(xiàn),他可以用來刷新Bean中的屬性配置,那大家對他的實(shí)現(xiàn)原理了解嗎,它為什么可以做到動態(tài)刷新呢,所以本文小編將給大家詳細(xì)介紹@RefreshScope注解實(shí)現(xiàn)動態(tài)刷新原理2023-07-07
Java使用Arrays.asList報(bào)UnsupportedOperationException的解決
這篇文章主要介紹了Java使用Arrays.asList報(bào)UnsupportedOperationException的解決,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
SpringBoot中的@Configuration注解詳解
這篇文章主要介紹了SpringBoot中的@Configuration注解詳解,Spring Boot推薦使用JAVA配置來完全代替XML 配置,JAVA配置就是通過 @Configuration和 @Bean兩個(gè)注解實(shí)現(xiàn)的,需要的朋友可以參考下2023-08-08
淺析Java關(guān)鍵詞synchronized的使用
Synchronized是java虛擬機(jī)為線程安全而引入的。這篇文章主要為大家介紹一下Java關(guān)鍵詞synchronized的使用與原理,需要的可以參考一下2022-12-12
很多人竟然不知道Java線程池的創(chuàng)建方式有7種
本文主要介紹了很多人竟然不知道Java線程池的創(chuàng)建方式有7種,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07
Springboot+Shiro+Mybatis+mysql實(shí)現(xiàn)權(quán)限安全認(rèn)證的示例代碼
Shiro是Apache?的一個(gè)強(qiáng)大且易用的Java安全框架,執(zhí)行身份驗(yàn)證、授權(quán)、密碼學(xué)和會話管理,Shiro?主要分為兩個(gè)部分就是認(rèn)證和授權(quán)兩部分,這篇文章主要介紹了Springboot+Shiro+Mybatis+mysql實(shí)現(xiàn)權(quán)限安全認(rèn)證的示例代碼,需要的朋友可以參考下2024-07-07
Java使用泛型Class實(shí)現(xiàn)消除模板代碼
Class作為實(shí)現(xiàn)反射功能的類,在開發(fā)中經(jīng)常會用到,然而,當(dāng)Class遇上泛型后,事情就變得不是那么簡單了,所以本文就來講講Java如何使用泛型Class實(shí)現(xiàn)消除模板代碼,需要的可以參考一下2023-06-06

