Springboot+Spring Security實現(xiàn)前后端分離登錄認證及權限控制的示例代碼
前言
關于Spring Security的概念部分本文不進行贅述,本文主要針對于對Spring Security以及Springboot有一定了解的小伙伴,幫助大家使用Springboot + Spring Security 實現(xiàn)一個前后端分離登錄認證的過程。
文章會一步一步循序漸進的帶大家敲一遍代碼。最終的代碼請看最后。
代碼中我用到了插件lombok來生成實體的getter/setter,如果不想裝插件請自己補全getter/setter
本文主要的功能
1、前后端分離用戶登錄認證
2、基于RBAC(角色)的權限控制
一、準備工作
1、統(tǒng)一錯誤碼枚舉
/** * @Author: Hutengfei * @Description: 返回碼定義 * 規(guī)定: * #1表示成功 * #1001~1999 區(qū)間表示參數(shù)錯誤 * #2001~2999 區(qū)間表示用戶錯誤 * #3001~3999 區(qū)間表示接口異常 * @Date Create in 2019/7/22 19:28 */ public enum ResultCode { /* 成功 */ SUCCESS(200, "成功"), /* 默認失敗 */ COMMON_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; } /** * 根據(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)一返回實體 * @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> <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 #實體掃描,多個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 #駝峰下劃線轉換 db-column-underline: true #刷新mapper 調試神器 refresh-mapper: true #數(shù)據(jù)庫大寫下劃線轉換 #capital-mode: true #序列接口實現(xiàn)類配置,不在推薦使用此方式進行配置,請使用自定義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 #自定義填充策略接口實現(xià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ù)庫表設計
建表語句
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 '創(chuàng)建時間', update_time datetime null comment '修改時間', 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 '權限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 '用戶角色關聯(liá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 '權限id' ) comment '角色-權限關聯(liá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 '權限id' ) comment '路徑權限關聯(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', '管理員', '管理員,擁有所有權限'); 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ù)我們會在里面加入一系列配置,包括配置認證方式、登入登出、異常處理、會話管理等。
@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、創(chuàng)建自定義UserDetailsService
這是實現(xiàn)自定義用戶認證的核心邏輯,loadUserByUsername(String username)的參數(shù)就是登錄時提交的用戶名,返回類型是一個叫UserDetails 的接口,需要在這里構造出他的一個實現(xiàn)類User,這是Spring security提供的用戶信息實體。
public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //需要構造出 org.springframework.security.core.userdetails.User 對象并返回 return null; } }
這里我們使用他的一個參數(shù)比較詳細的構造函數(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):用戶權限列表
這就與我們的創(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
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) { //獲取該用戶所擁有的權限 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)過期失效)
/** * @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、實現(xiàn)登錄成功/失敗、登出處理邏輯
首先需要明白一件事,對于登入登出我們都不需要自己編寫controller接口,Spring Security為我們封裝好了。默認登入路徑:/login,登出路徑:/logout。當然我們可以也修改默認的名字。登錄成功失敗和登出的后續(xù)處理邏輯如何編寫會在后面慢慢解釋。
當?shù)卿洺晒虻卿浭《夹枰祷亟y(tǒng)一的json返回體給前臺,前臺才能知道對應的做什么處理。
而實現(xiàn)登錄成功和失敗的異常處理需要分別實現(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 { //更新用戶表上次登錄時間、更新人、更新時間等字段 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); //此處還可以進行一些處理,比如登錄成功之后可能需要返回給前臺當前用戶有哪些菜單權限, //進而前臺動態(tài)的控制菜單的顯示等,具體根據(jù)自己的業(yè)務需求進行擴展 //返回json數(shù)據(jù) JsonResult result = ResultTool.success(); //處理編碼方式,防止中文亂碼的情況 httpServletResponse.setContentType("text/json;charset=utf-8"); //塞到HttpServletResponse中返回給前臺 httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }
(2)登錄失敗
登錄失敗處理器主要用來對登錄失敗的場景(密碼錯誤、賬號鎖定等…)做統(tǒng)一處理并返回給前臺統(tǒng)一的json返回體。還記得我們創(chuàng)建用戶表的時候創(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) { //密碼錯誤 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, 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
效果如圖:
登錄時密碼錯誤
登錄時賬號被鎖定
退出登錄之后再次請求資源接口
八、會話管理(登錄過時、限制單用戶或多用戶登錄等)
1、限制登錄用戶數(shù)量
比如限制同一賬號只能一個用戶使用
and().sessionManagement(). maximumSessions(1)
2、處理賬號被擠下線處理邏輯
同樣的,當賬號異地登錄導致被擠下線時也要返回給前端json格式的數(shù)據(jù),比如提示"賬號下線"、"您的賬號在異地登錄,是否是您自己操作"或者"您的賬號在異地登錄,可能由于密碼泄露,建議修改密碼"等。這時就要實現(xiàn)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(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、權限攔截器
/** * @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 { //執(zhí)行下一個攔截器 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(); //查詢具體某個接口的權限 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
/** * @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);//安全元數(shù)據(jù)源 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; //實現(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); } }
十一、結束語
到現(xiàn)在為止本文就基本結束了,在本文中我們利用Springboot+Spring security實現(xiàn)了前后端分離的用戶登錄認證和動態(tài)的權限訪問控制。
最后附上github地址:
github
到此這篇關于Springboot+Spring Security實現(xiàn)前后端分離登錄認證及權限控制的示例代碼的文章就介紹到這了,更多相關Springboot SpringSecurity前后端分離登錄認證內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- Spring Security 自定義短信登錄認證的實現(xiàn)
- Java SpringSecurity+JWT實現(xiàn)登錄認證
- SpringBoot security安全認證登錄的實現(xiàn)方法
- SpringSecurity實現(xiàn)前后端分離登錄token認證詳解
- Springboot整合SpringSecurity實現(xiàn)登錄認證和鑒權全過程
- springsecurity實現(xiàn)用戶登錄認證快速使用示例代碼(前后端分離項目)
- Spring Security實現(xiàn)登錄認證實戰(zhàn)教程
- SpringSecurity 自定義認證登錄的項目實踐
- spring security登錄認證授權的項目實踐
相關文章
IDEA配置Tomcat創(chuàng)建web項目的詳細步驟
Tomcat是一個Java?Web應用服務器,實現(xiàn)了多個Java?EE規(guī)范(JSP、Java?Servlet等),這篇文章主要給大家介紹了關于IDEA配置Tomcat創(chuàng)建web項目的詳細步驟,需要的朋友可以參考下2023-12-12解決springboot 2.x 里面訪問靜態(tài)資源的坑
這篇文章主要介紹了解決springboot 2.x 里面訪問靜態(tài)資源的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08舉例說明JAVA調用第三方接口的GET/POST/PUT請求方式
在日常工作和學習中,有很多地方都需要發(fā)送請求,這篇文章主要給大家介紹了關于JAVA調用第三方接口的GET/POST/PUT請求方式的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-01-01SpringBoot實現(xiàn)阿里云短信發(fā)送的示例代碼
這篇文章主要為大家介紹了如何利用SpringBoot實現(xiàn)阿里云短信發(fā)送,文中的示例代碼講解詳細,對我們學習或工作有一定幫助,需要的可以參考一下2022-04-04ssm框架controller層返回json格式數(shù)據(jù)到頁面的實現(xiàn)
這篇文章主要介紹了ssm框架controller層返回json格式數(shù)據(jù)到頁面的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-09-09