第四章:Springboot + Spring Security 實現(xiàn)前后端分離登錄認證及權限控制

此篇文章轉載于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ù)庫表設計


image.png
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方式進行加密。


image.png

在WebSecurityConfig 中注入(注入即可,不用聲明使用),這樣就會對提交的密碼進行加密處理了,如果你沒有注入加密方式,運行的時候會報錯"There is no PasswordEncoder mapped for the id"錯誤。

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        // 設置默認的加密方式(強hash方式加密)
        return new BCryptPasswordEncoder();
    }

同樣的我們數(shù)據(jù)庫里存儲的密碼也要用同樣的加密方式存儲,例如我們將123456用BCryptPasswordEncoder 加密后存儲到數(shù)據(jù)庫中(注意:即使是同一個明文用這種加密方式加密出來的密文也是不同的,這就是這種加密方式的特點)


image.png

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


image.png

然后規(guī)定接口"/getUser"只能是擁有"query_user"權限的用戶可以訪問。后面我們基本都用這個查詢接口作為演示,就叫它"資源接口"吧。

http.authorizeRequests().
       antMatchers("/getUser").hasAuthority("query_user").

演示登錄時,如果用戶沒有登錄去請求資源接口就會提示未登錄
????在前后端不分離的時候當用戶未登錄去訪問資源時Spring security會重定向到默認的登錄頁面,返回的是一串html標簽,這一串html標簽其實就是登錄頁面的提交表單。如圖所示


image.png

而在前后端分離的情況下(比如前臺使用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).//匿名用戶訪問無權限資源時的異常處理

再次請求資源接口


image.png

前臺拿到這個錯誤時就可以做一些處理了,主要是退出到登錄頁面。

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

效果如圖:
登錄時密碼錯誤


image.png

登錄時賬號被鎖定


image.png

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


image.png

八、會話管理(登錄過時、限制單用戶或多用戶登錄等)
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登錄
我電腦上請求資源接口,可以請求,如下左圖
在旁邊電腦上再登錄一次剛剛的賬號
在我電腦上再次請求資源接口,提示"賬號下線",如右下圖

image.png

九、實現(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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容