基於SpringSecurity和JWT的用戶訪問認證和受權

發佈時間:2018-12-03
 
技術:springsecurity+jwt+java+jpa+mysql+mysql workBench
 

概述

基於SpringSecurity和JWT的用戶訪問認證和受權。根據現實案例,先後端分離,而且後端爲分佈式部署。解決redis session共享方式的跨域問題,解決單一使用security時每次訪問資源都須要用戶信息進行登陸的效率問題和安全問題。

詳細

1、新建springboot項目

1.配置數據庫鏈接。 確保項目成功運行,並能訪問。html

2.引入springsecurity依賴和JWT依賴。java

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.6.0</version>
</dependency>

3.啓動項目,瀏覽器訪問http://localhost:8080,會跳出登陸界面。這是springsecurity的默認登陸界面。也可使用本身的登陸頁面。mysql

使用默認的用戶名:user,和啓動項目時生成的密碼 啓動時日誌打印的密碼.png,便可登陸。web

2、配置spring security

1.新建WebSecurityConfig.java做爲配置類,繼承WebSecurityConfigurationAdapter類,重寫三個configure方法。在這個配置類上添加@EnableWebSecurityConfig,@EnableGlobaleMethodSecurity註解並設置屬性prePostEnable = true。正則表達式

package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
}

@EnableWebSecurityConfig:該註解和@Configuration註解一塊兒使用,註解 WebSecurityConfigurer類型的類,或者利用@EnableWebSecurity註解繼承WebSecurityConfigurerAdapter的類,這樣就構成了Spring Security的配置。WebSecurityConfigurerAdapter 提供了一種便利的方式去建立 WebSecurityConfigurer的實例,只須要重寫 WebSecurityConfigurerAdapter 的方法,便可配置攔截什麼URL、設置權限等安全控制。redis

@EnableGlobaleMethodSecurity: Spring security默認是禁用註解的,要想開啓註解,須要繼承WebSecurityConfigurerAdapter的類上加@EnableGlobalMethodSecurity註解,來判斷用戶對某個控制層的方法是否具備訪問權限。算法

①開啓@Secured註解過濾權限: @EnableGlobalMethodSecurity(securedEnabled=true)spring

@Secured:認證是否有權限訪問。eg:sql

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id){
    ...
}

@Secured("ROLE_TELLER")
public Account readAccount(Long id){
    ...
}

②開啓@RolesAllowed註解過濾權限:@EnableGolablMethodSecurity(jsr250Enabled=true)數據庫

@DenyAll:拒絕全部訪問。

@RolesAllowed({"USER","ADMIN"}):該方法只要具備USER,ADMIN任意一種權限就能夠訪問(能夠省略前綴ROLE_)。

@PermitAll:容許全部訪問。

③使用Spring_EL表達式控制更細粒度的訪問控制:@EnableGlobalMethodSecurity(prePostEnable=true)

@PreAuthoriz:在方法執行以前執行,並且這裏能夠調用方法的參數,也能夠獲得參數值。這是利用java8的參數名反射特性,若是沒用java8,那麼也能夠利用Spring Security的@P標註參數,或者Spring Data的@Param標註參數。eg:

@PreAuthorize("#userId==authentication.principal.userId or hasAuthority('ADMIN')")
public void changPassword(@P("userId")long userId){
    ...
}

@PreAuthorize("hasAuthority('ADMIN')")
public void changePassword(long userId){
    ...
}

@PostAuthorize:在方法執行以後執行,並且這裏能夠調用方法的返回值,若是EL爲false,那麼該方法也已經執行完了,可能會回滾。EL變量returnObject表示返回的對象。EL表達式計算結果爲false將拋出一個安全性異常。eg:

@PostAuthorize("returnObject.userId==authentication.principal.userId or hasPermission(returnObject,'ADMIN')")
User getUser(){
    ...
}

@PreFilter:在方法執行以前執行,並且這裏能夠調用方法的參數,而後對參數值進行過濾或處理,EL變量filterObject表示參數,若是有多個參數,使用filterTarget註解參數。只有方法參數是集合或數組才行。

@PostFilter:在方法執行以後執行,並且這裏能夠經過表達式來過濾方法的結果。

configure(AuthenticationManagerBuilder auth):

configure(HttpSecurity http):

configure(WebSecurity web):

3、建立RBAC模型中 數據庫表的模型

使用mysql workBench 或者 Power Design 建立表關係物理模型導出爲SQL腳本,再執行SQL腳本便可建立,或者直接編寫DDL語句。

也可使用JPA經過在實體上使用註解,在程序啓動時自動生成表和設置表關係。(配置spring.jpa.ddl-auto=update)。

------------------------方便理清表結構和關係,這裏使用了mysql workBench 繪製了表結構關係物理模型----------------------------

1.表模型:

image.png

2.數據庫語句:

-- MySQL Script generated by MySQL Workbench
-- 11/28/18 16:15:02
-- Model: New Model    Version: 1.0
-- MySQL Workbench Forward Engineering

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';

-- -----------------------------------------------------
-- Schema security
-- -----------------------------------------------------
DROP SCHEMA IF EXISTS `security` ;

-- -----------------------------------------------------
-- Schema security
-- -----------------------------------------------------
CREATE SCHEMA IF NOT EXISTS `security` DEFAULT CHARACTER SET utf8 ;
USE `security` ;

-- -----------------------------------------------------
-- Table `security`.`user`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `security`.`user` ;

CREATE TABLE IF NOT EXISTS `security`.`user` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `user_name` VARCHAR(45) NOT NULL,
  `user_no` VARCHAR(45) NOT NULL,
  `password` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `user_no_UNIQUE` (`user_no` ASC))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;


-- -----------------------------------------------------
-- Table `security`.`role`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `security`.`role` ;

CREATE TABLE IF NOT EXISTS `security`.`role` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `role_name` VARCHAR(45) NOT NULL,
  `role_no` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `role_name_UNIQUE` (`role_name` ASC),
  UNIQUE INDEX `role_no_UNIQUE` (`role_no` ASC))
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `security`.`permission`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `security`.`permission` ;

CREATE TABLE IF NOT EXISTS `security`.`permission` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `permission_name` VARCHAR(45) NOT NULL,
  `permission_no` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `permission_name_UNIQUE` (`permission_name` ASC),
  UNIQUE INDEX `permission_no_UNIQUE` (`permission_no` ASC))
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `security`.`parent_menu`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `security`.`parent_menu` ;

CREATE TABLE IF NOT EXISTS `security`.`parent_menu` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `parent_menu_name` VARCHAR(45) NOT NULL,
  `parent_menu_no` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `menu_name_UNIQUE` (`parent_menu_name` ASC),
  UNIQUE INDEX `menu_no_UNIQUE` (`parent_menu_no` ASC))
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `security`.`child_menu`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `security`.`child_menu` ;

CREATE TABLE IF NOT EXISTS `security`.`child_menu` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `child_menu_name` VARCHAR(45) NOT NULL,
  `child_menu_no` VARCHAR(45) NOT NULL,
  `parent_menu_id` INT NOT NULL,
  PRIMARY KEY (`id`, `parent_menu_id`),
  UNIQUE INDEX `child_menu_name_UNIQUE` (`child_menu_name` ASC),
  UNIQUE INDEX `child_menu_no_UNIQUE` (`child_menu_no` ASC),
  INDEX `fk_child_menu_parent_menu_idx` (`parent_menu_id` ASC),
  CONSTRAINT `fk_child_menu_parent_menu`
    FOREIGN KEY (`parent_menu_id`)
    REFERENCES `security`.`parent_menu` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE)
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `security`.`user_role`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `security`.`user_role` ;

CREATE TABLE IF NOT EXISTS `security`.`user_role` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `user_id` INT NOT NULL,
  `role_id` INT NOT NULL,
  PRIMARY KEY (`id`, `user_id`, `role_id`),
  INDEX `fk_user_role_user1_idx` (`user_id` ASC),
  INDEX `fk_user_role_role1_idx` (`role_id` ASC),
  CONSTRAINT `fk_user_role_user1`
    FOREIGN KEY (`user_id`)
    REFERENCES `security`.`user` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
  CONSTRAINT `fk_user_role_role1`
    FOREIGN KEY (`role_id`)
    REFERENCES `security`.`role` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE)
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `security`.`role_permission`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `security`.`role_permission` ;

CREATE TABLE IF NOT EXISTS `security`.`role_permission` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `role_id` INT NOT NULL,
  `permission_id` INT NOT NULL,
  PRIMARY KEY (`id`, `role_id`, `permission_id`),
  INDEX `fk_role_permission_role1_idx` (`role_id` ASC),
  INDEX `fk_role_permission_permission1_idx` (`permission_id` ASC),
  CONSTRAINT `fk_role_permission_role1`
    FOREIGN KEY (`role_id`)
    REFERENCES `security`.`role` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
  CONSTRAINT `fk_role_permission_permission1`
    FOREIGN KEY (`permission_id`)
    REFERENCES `security`.`permission` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE)
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `security`.`permission_parent_menu`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `security`.`permission_parent_menu` ;

CREATE TABLE IF NOT EXISTS `security`.`permission_parent_menu` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `permission_id` INT NOT NULL,
  `parent_menu_id` INT NOT NULL,
  PRIMARY KEY (`id`, `permission_id`, `parent_menu_id`),
  INDEX `fk_permission_parent_menu_permission1_idx` (`permission_id` ASC),
  INDEX `fk_permission_parent_menu_parent_menu1_idx` (`parent_menu_id` ASC),
  CONSTRAINT `fk_permission_parent_menu_permission1`
    FOREIGN KEY (`permission_id`)
    REFERENCES `security`.`permission` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
  CONSTRAINT `fk_permission_parent_menu_parent_menu1`
    FOREIGN KEY (`parent_menu_id`)
    REFERENCES `security`.`parent_menu` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE)
ENGINE = InnoDB;


SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

3.執行sql,生成表。

image.png

-------------------------------------------------------使用JPA註解方式-------------------------------------------------------------

User:

package com.example.demo.domain;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@Entity
public class User implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "user_name")
    private String userName;

    @Column(name = "user_no")
    private String userNo;

    private String password;

    @ManyToMany(cascade = CascadeType.PERSIST,fetch = FetchType.LAZY)
    @JoinTable(name = "user_role",joinColumns = @JoinColumn(name = "user_id",referencedColumnName = "id"),
    inverseJoinColumns = @JoinColumn(name = "role_id",referencedColumnName = "id"))
    private List<Role> roles;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", userName='" + userName + '\'' +
                ", userNo='" + userNo + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

Role:

package com.example.demo.domain;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@Entity
public class Role implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "role_name")
    private String roleName;

    @Column(name = "role_no")
    private String roleNo;

    /**
     * mappedBy表示role爲關係的被維護端
     */
    @ManyToMany(mappedBy = "roles",fetch = FetchType.LAZY)
    @JsonIgnore
    private List<User> users;

    @ManyToMany(cascade = {CascadeType.PERSIST})
    @JoinTable(name = "role_permission",joinColumns = @JoinColumn(name = "role_id",referencedColumnName = "id"),
    inverseJoinColumns = @JoinColumn(name = "permission_id",referencedColumnName = "id"))
    private List<Permission> permissions;

    @Override
    public String toString() {
        return "Role{" +
                "id=" + id +
                ", roleName='" + roleName + '\'' +
                ", roleNo='" + roleNo + '\'' +
                '}';
    }
}

Permission:

package com.example.demo.domain;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@Entity
public class Permission implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "permission_name")
    private String permissionName;

    @Column(name = "permission_no")
    private String permissionNo;

    @ManyToMany(mappedBy = "permissions",fetch = FetchType.LAZY)
    @JsonIgnore
    private List<Role> roles;

    @ManyToMany(cascade = {CascadeType.PERSIST},fetch = FetchType.LAZY)
    @JoinTable(name = "permission_parent_menu",joinColumns = @JoinColumn(name = "permission_id",referencedColumnName = "id"),
    inverseJoinColumns = @JoinColumn(name = "parent_menu_id",referencedColumnName = "id"))
    private List<ParentMenu> parentMenus;

    @Override
    public String toString() {
        return "Permission{" +
                "id=" + id +
                ", permissionName='" + permissionName + '\'' +
                ", permissionNo='" + permissionNo + '\'' +
                '}';
    }
}

ParentMenu:

package com.example.demo.domain;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@Entity
@Table(name = "parent_menu")
public class ParentMenu implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "parent_menu_name")
    private String parentMenuName;

    @Column(name = "parent_menu_no")
    private String parentMenuNo;

    @ManyToMany(mappedBy = "parentMenus",fetch = FetchType.LAZY)
    @JsonIgnore
    private List<Permission> permissions;

    @OneToMany(mappedBy = "parentMenu",cascade = {CascadeType.ALL},fetch = FetchType.LAZY)
    private List<ChildMenu> childMenus;

    @Override
    public String toString() {
        return "ParentMenu{" +
                "id=" + id +
                ", parentMenuName='" + parentMenuName + '\'' +
                ", parentMenuNo='" + parentMenuNo + '\'' +
                '}';
    }
}

ChildMenu:

package com.example.demo.domain;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;

@Data
@NoArgsConstructor
@Entity
@Table(name = "child_menu")
public class ChildMenu implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "child_menu_name")
    private String childMenuName;

    @Column(name = "child_menu_no")
    private String childMenuNo;

    @ManyToOne(fetch = FetchType.LAZY,optional = false)
    @JoinColumn(name = "parent_menu_id",referencedColumnName = "id")
    @JsonIgnore
    private ParentMenu parentMenu;

    /**
     * 避免無限遞歸,內存溢出
     * @return
     */
    @Override
    public String toString() {
        return "ChildMenu{" +
                "id=" + id +
                ", childMenuName='" + childMenuName + '\'' +
                ", childMenuNo='" + childMenuNo + '\'' +
                '}';
    }
}

4、建立DAO接口,測試保存,查詢,刪除

1.給每個實體建立一個DAO接口來操做實體對應的數據庫表。

package com.example.demo.dao;

import com.example.demo.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserDao extends JpaRepository<User,Integer>{

}

2.保存測試數據

public void addData() {
    ChildMenu childMenu1 = new ChildMenu();
    childMenu1.setChildMenuName("child_menu_1_1");
    childMenu1.setChildMenuNo("1-1");
    ChildMenu childMenu2 = new ChildMenu();
    childMenu2.setChildMenuName("child_menu_1_2");
    childMenu2.setChildMenuNo("1-2");
    ChildMenu childMenu3 = new ChildMenu();
    childMenu3.setChildMenuName("child_menu_2_1");
    childMenu3.setChildMenuNo("2-1");
    ChildMenu childMenu4 = new ChildMenu();
    childMenu4.setChildMenuName("child_menu_2_2");
    childMenu4.setChildMenuNo("2-2");
    ChildMenu childMenu5 = new ChildMenu();
    childMenu5.setChildMenuName("child_menu_3_1");
    childMenu5.setChildMenuNo("3-1");
    ParentMenu parentMenu1 = new ParentMenu();
    ParentMenu parentMenu2 = new ParentMenu();
    ParentMenu parentMenu3 = new ParentMenu();
    parentMenu1.setParentMenuName("parent_menu_1");
    parentMenu1.setParentMenuNo("1");
    parentMenu1.setChildMenus(Arrays.asList(childMenu1, childMenu2));
    childMenu1.setParentMenu(parentMenu1);
    childMenu2.setParentMenu(parentMenu1);
    parentMenu2.setParentMenuName("parent_menu_2");
    parentMenu2.setParentMenuNo("2");
    parentMenu2.setChildMenus(Arrays.asList(childMenu3, childMenu4));
    childMenu3.setParentMenu(parentMenu2);
    childMenu4.setParentMenu(parentMenu2);
    parentMenu3.setParentMenuName("parent_menu_3");
    parentMenu3.setParentMenuNo("3");
    parentMenu3.setChildMenus(Arrays.asList(childMenu1,childMenu2,childMenu3,childMenu4,childMenu5));
    childMenu5.setParentMenu(parentMenu3);
    Permission permission1 = new Permission();
    Permission permission2 = new Permission();
    Permission permission3 = new Permission();
    Role role1 = new Role();
    Role role2 = new Role();
    Role role3 = new Role();
    User user1 = new User();
    User user2 = new User();
    User user3 = new User();
    User user4 = new User();
    permission1.setPermissionName("p_1");
    permission1.setPermissionNo("1");
    permission1.setParentMenus(Arrays.asList(parentMenu1));
    permission2.setPermissionName("p_2");
    permission2.setPermissionNo("2");
    permission2.setParentMenus(Arrays.asList(parentMenu2));
    permission3.setPermissionName("p_3");
    permission3.setPermissionNo("3");
    permission3.setParentMenus(Arrays.asList(parentMenu3));
    role1.setRoleName("管理員");
    role1.setRoleNo("admin");
    role1.setPermissions(Arrays.asList(permission1,permission2,permission3));
    role2.setRoleName("普通用戶角色1");
    role2.setRoleNo("role1");
    role2.setPermissions(Arrays.asList(permission1));
    role3.setRoleName("普通用戶角色2");
    role3.setRoleNo("role2");
    role3.setPermissions(Arrays.asList(permission2));
    user1.setUserName("user1");
    user1.setUserNo("NO1");
    user1.setPassword("123456");
    user1.setRoles(Arrays.asList(role1,role2,role3));
    user2.setUserName("user2");
    user2.setUserNo("NO2");
    user2.setPassword("123456");
    user2.setRoles(Arrays.asList(role2));
    user3.setUserName("user3");
    user3.setUserNo("NO3");
    user3.setPassword("123456");
    user3.setRoles(Arrays.asList(role3));
    user4.setUserName("user4");
    user4.setUserNo("NO4");
    user4.setPassword("123456");
    user4.setRoles(Arrays.asList(role1));
    userDao.save(user1);
    userDao.save(user2);
    userDao.save(user3);
    userDao.save(user4);
}

3.查詢測試數據

@Override
public void getData() {
    Optional<User> userOptional = userDao.findById(1);
    User user = null;
    if (userOptional.isPresent()){
        user = userOptional.get();
    }
    System.out.println(user);
    List<Role> roles = user.getRoles();
    for (Role role : roles){
        System.out.println(role);
        List<User> users = role.getUsers();
        for (User user1: users){
            System.out.println("--"+user1);
        }
        System.out.println("------------------------------------------------------------------");
        List<Permission> permissions = role.getPermissions();
        for (Permission permission : permissions){
            System.out.println(permission);
            System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
            List<ParentMenu> parentMenus = permission.getParentMenus();
            for (ParentMenu parentMenu : parentMenus){
                System.out.println(parentMenu);
                System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
                List<ChildMenu> childMenus = parentMenu.getChildMenus();
                for (ChildMenu childMenu : childMenus){
                    System.out.println(childMenu);
                    System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
                }
            }
        }
    }
}

4.刪除測試數據

@Override
public void deleteData() {
    userDao.deleteById(4);
    Optional<Role> roleOptional = roleDAO.findById(1);
    if (!roleOptional.isPresent()){
        System.out.println("刪除出錯,刪除用戶不能級聯刪除角色");
    }
    parentMenuDao.deleteById(1);
    Optional<Permission> permissionOptional = permissionDao.findById(1);
    if (permissionOptional.isPresent()){
        Permission permission = permissionOptional.get();
        List<Role> roles = permission.getRoles();
        if (roles == null || roles.size() == 0){
            System.out.println("該權限沒有角色擁有");
        }else {
            for (Role role:roles){
                System.out.println(role);
            }
        }
        List<ParentMenu> parentMenus = permission.getParentMenus();
        if (parentMenus == null || parentMenus.size() == 0){
            System.out.println("該權限沒有能夠查看的菜單");
        }else {
            for (ParentMenu parentMenu:parentMenus){
                System.out.println(parentMenu);
            }
        }
    }
}

注意!!!!

在刪除這裏可能會有問題。在從表中刪除記錄會致使失敗。須要設置外鍵約束ON DELETE和ON UPDATE。

 

On Delete和On Update都有Restrict,No Action, Cascade,Set Null屬性。如今分別對他們的屬性含義作個解釋。
ON DELETE
restrict(約束):當在父表(即外鍵的來源表)中刪除對應記錄時,首先檢查該記錄是否有對應外鍵,若是有則不容許刪除。

no action:意思同restrict.即若是存在從數據,不容許刪除主數據。

cascade(級聯):當在父表(即外鍵的來源表)中刪除對應記錄時,首先檢查該記錄是否有對應外鍵,若是有則也刪除外鍵在子表(即包含外鍵的表)中的記錄。

set null:當在父表(即外鍵的來源表)中刪除對應記錄時,首先檢查該記錄是否有對應外鍵,若是有則設置子表中該外鍵值爲null(不過這就要求該外鍵容許取null)

ON UPDATE
restrict(約束):當在父表(即外鍵的來源表)中更新對應記錄時,首先檢查該記錄是否有對應外鍵,若是有則不容許更新。

no action:意思同restrict.

cascade(級聯):當在父表(即外鍵的來源表)中更新對應記錄時,首先檢查該記錄是否有對應外鍵,若是有則也更新外鍵在子表(即包含外鍵的表)中的記錄。

set null:當在父表(即外鍵的來源表)中更新對應記錄時,首先檢查該記錄是否有對應外鍵,若是有則設置子表中該外鍵值爲null(不過這就要求該外鍵容許取null)。

注:NO ACTION和RESTRICT的區別:只有在及個別的狀況下會致使區別,前者是在其餘約束的動做以後執行,後者具備最高的優先權執行。

在這裏每個表都須要能夠單獨維護,因此設置ON DELETE 和 ON UPDATE 都爲cascade。

使用mysql workBench的設置方法:從新導出SQL腳本,從新執行便可。

image.png在Navicat for mysql中的設置方法:選擇存在外鍵的表,設計表,外鍵。

image.png

至此,數據庫表已經創建完成。

5、自定義關機類

UserDetails,UserDetailsService,Provider,UsernamePasswordAuthenticationFilter,BasicAuthenticationFilter,LogoutFilter

1.MyUserDetails:繼承User類並實現UserDetails接口。

package com.example.demo.domain;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class MyUserDetails extends User implements UserDetails{

    public MyUserDetails(User user) {
        super(user);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        List<Permission> permissions = new ArrayList<>();
        List<Role> roles = super.getRoles();
        for (Role role : roles){
            List<Permission> permissionList = role.getPermissions();
            if (permissionList != null || permissionList.size() != 0){
                for (Permission permission:permissionList){
                    if (!permissions.contains(permission)){
                        permissions.add(permission);
                    }
                }
            }
        }
        if (permissions == null || permissions.size() == 0){

        }else {
            for (Permission permission:permissions){
                //這裏使用的是權限名稱,也可使用權限id或者編號。區別在於在使用@PreAuthorize("hasAuthority('權限名稱')")
                authorities.add(new SimpleGrantedAuthority(permission.getPermissionName()));
            }
        }
        return authorities;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public String getPassword() {
        return super.getPassword();
    }

    @Override
    public String getUsername() {
        return super.getUserName();
    }
}

2.MyUserDetailsService:繼承UserDetailsService接口。

package com.example.demo.service.impl;

import com.example.demo.dao.UserDao;
import com.example.demo.domain.MyUserDetails;
import com.example.demo.domain.Permission;
import com.example.demo.domain.Role;
import com.example.demo.domain.User;
import com.example.demo.exception.WrongUsernameException;
import com.example.demo.util.AuthErrorEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Slf4j
public class MyUserDetailsService implements UserDetailsService{

    @Autowired
    private UserDao userDao;

    /**
     * 根據用戶名登陸
     * @param s 用戶名
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Optional<User> userOptional = userDao.findUserByUserName(s);
        if (userOptional.isPresent()){
            User user = userOptional.get();
            //級聯查詢
            List<Role> roles = user.getRoles();
            List<Permission> permissions = new ArrayList<>();
            for (Role role:roles){
                //級聯查詢
                List<Permission> permissionList = role.getPermissions();
//                role.setPermissions(permissionList);
            }
//            user.setRoles(roles);
            UserDetails userDetails = new MyUserDetails(user);
//            List<GrantedAuthority> authorities = (List<GrantedAuthority>) userDetails.getAuthorities();
            return userDetails;
        }else {
            log.error("用戶不存在");
            throw new WrongUsernameException(AuthErrorEnum.LOGIN_NAME_ERROR.getMessage());
        }
    }
}

3.MyAuthenticationProvider:實現AuthenticationProvider接口

package com.example.demo.filter;

import com.example.demo.domain.MyUserDetails;
import com.example.demo.exception.WrongPasswordException;
import com.example.demo.exception.WrongUsernameException;
import com.example.demo.util.AuthErrorEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import java.util.Collection;

@Slf4j
public class MyAuthenticationProvider implements AuthenticationProvider{

    private UserDetailsService userDetailsService;

    private BCryptPasswordEncoder passwordEncoder;

    public MyAuthenticationProvider(UserDetailsService userDetailsService, BCryptPasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return aClass.equals(UsernamePasswordAuthenticationToken.class);
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //authentication,登陸url提交的須要被認證的對象。只含有用戶名和密碼,須要根據用戶名和密碼來校驗,而且受權。
//        MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();
//        String userName = myUserDetails.getUserName();
//        String password = myUserDetails.getPassword();
        String userName = authentication.getName();
        String password = (String) authentication.getCredentials();
        MyUserDetails userDetails = (MyUserDetails) userDetailsService.loadUserByUsername(userName);
        if (userDetails == null){
            log.warn("User not found with userName:{}",userName);
            throw new WrongUsernameException(AuthErrorEnum.LOGIN_NAME_ERROR.getMessage());
        }
        //若是從url提交的密碼到數據保存的密碼沒有通過加密或者編碼,直接比較是否相同便可。若是在添加用戶時的密碼是通過加密或者編碼的應該使用對應的加密算法和編碼工具對密碼進行編碼以後再進行比較
//        if (!passwordEncoder.matches(password, userDetails.getPassword())){
//            log.warn("Wrong password");
//            throw new WrongPasswordException(AuthErrorEnum.LOGIN_PASSWORD_ERROR.getMessage());
//        }
        if (!password.equals(userDetails.getPassword())){
            log.warn("Wrong password");
            throw new WrongPasswordException(AuthErrorEnum.LOGIN_PASSWORD_ERROR.getMessage());
        }
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        return new UsernamePasswordAuthenticationToken(userDetails,password,authorities);
    }
}

4.MyLoginFilter:繼承 UsernamePasswordAuthenticationFilter。只過濾/login,方法必須爲POST

package com.example.demo.filter;

import com.example.demo.dao.PermissionDao;
import com.example.demo.domain.*;
import com.example.demo.util.GetPostRequestContentUtil;
import com.example.demo.util.JwtUtil;
import com.example.demo.util.ObjectMapperUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Slf4j
public class MyLoginFilter extends UsernamePasswordAuthenticationFilter{

    private AuthenticationManager authenticationManager;

    private String head;

    private String tokenHeader;

    private PermissionDao permissionDao;

    public MyLoginFilter(AuthenticationManager authenticationManager,String head,String tokenHeader,PermissionDao permissionDao) {
        this.authenticationManager = authenticationManager;
        this.head = head;
        this.tokenHeader = tokenHeader;
        this.permissionDao = permissionDao;
    }


    /**
     * 接收並解析用戶登錄信息  /login,必須使用/login,和post方法纔會進入此filter
     *若是身份驗證過程失敗,就拋出一個AuthenticationException
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //從request中獲取username和password,並封裝成user
        String body =  new GetPostRequestContentUtil().getRequestBody(request);
        User user = (User) ObjectMapperUtil.readValue(body,User.class);
        if (user == null){
            log.error("解析出錯");
            return null;
        }
        String userName = user.getUserName();
        String password = user.getPassword();
        log.info("用戶(登陸名):{} 正在進行登陸驗證。。。密碼:{}",userName,password);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName,password);
        //提交給自定義的provider組件進行身份驗證和受權
        Authentication authentication = authenticationManager.authenticate(token);
        return authentication;
    }

    /**
     * 驗證成功後,此方法會被調用,在此方法中生成token,並返回給客戶端
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        //設置安全上下文。在當前的線程中,任何一處均可以經過SecurityContextHolder來獲取當前用戶認證成功的Authentication對象
        SecurityContextHolder.getContext().setAuthentication(authResult);
        MyUserDetails userDetails = (MyUserDetails) authResult.getPrincipal();
        //使用JWT快速生成token
        String token = JwtUtil.setClaim(userDetails.getUsername(),true,60*60*1000);
        //根據當前用戶的權限能夠獲取當前用戶能夠查看的父菜單以及子菜單。(這裏在UserDetailsService中因爲級聯查詢,該用戶下的全部信息已經查出)
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        List<ParentMenu> parentMenus = new ArrayList<>();
        for (GrantedAuthority authority : authorities){
            String permissionName = authority.getAuthority();
            Permission permission = permissionDao.findPermissionByPermissionName(permissionName);
            List<ParentMenu> parentMenuList = permission.getParentMenus();
            for (ParentMenu parentMenu : parentMenuList){
                if (!parentMenus.contains(parentMenu)){
                    parentMenus.add(parentMenu);
                }
            }
        }
        //返回在response header 中返回token,而且返回用戶能夠查看的菜單數據
        response.setHeader(tokenHeader,head+token);
        response.setCharacterEncoding("utf-8");
        response.getWriter().write(ObjectMapperUtil.writeAsString(parentMenus));
    }
}

5.在security配置類中添加配置:

package com.example.demo.config;

import com.example.demo.dao.PermissionDao;
import com.example.demo.filter.MyAccessDeniedHandler;
import com.example.demo.filter.MyAuthenticationProvider;
import com.example.demo.filter.MyExceptionHandleFilter;
import com.example.demo.filter.MyLoginFilter;
import com.example.demo.service.impl.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.web.authentication.logout.LogoutFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.head}")
    private String head;

    @Value("${jwt.expired}")
    private boolean expired;

    @Value("${jwt.expiration}")
    private int expiration;

    @Value("${jwt.permitUris}")
    private String permitUris;

    @Autowired
    private PermissionDao permissionDao;

    @Bean
    public UserDetailsService myUserDetailsService(){
        return new MyUserDetailsService();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new MyAuthenticationProvider(myUserDetailsService(),passwordEncoder()));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .cors()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers().permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(new MyLoginFilter(authenticationManager(),head,tokenHeader,permissionDao));
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
}

至此,用戶的登陸認證,受權,和token頒發已經所有完成。詳細流程以下圖:

1543808543(1).jpg

使用postman測試登陸接口:

image.png

返回:

image.pngimage.png

5.MyAuthenticationFilter,繼承BasicAuthenticationFilter。過濾其餘的URL請求。(登陸邏輯也可在這裏處理)

package com.example.demo.filter;

import com.example.demo.exception.IllegalTokenAuthenticationException;
import com.example.demo.exception.NoneTokenException;
import com.example.demo.exception.TokenIsExpiredException;
import com.example.demo.util.AuthErrorEnum;
import com.example.demo.util.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.regex.Pattern;

/**
 * 除了/login,/logout,全部URI都會進入
 * token驗證過濾器
 */
@Slf4j
public class MyAuthenticationFilter extends BasicAuthenticationFilter {

    private String tokenHeader;

    private String head;

    private UserDetailsService userDetailsService;

    public MyAuthenticationFilter(AuthenticationManager authenticationManager, String tokenHeader, String head, UserDetailsService userDetailsService) {
        super(authenticationManager);
        this.head = head;
        this.tokenHeader = tokenHeader;
        this.userDetailsService = userDetailsService;
    }

    /**
     * 判斷請求是不是否帶有token信息,token是否合法,是否過時。設置安全上下文。
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = request.getHeader(tokenHeader);
        //多是登陸或者註冊的請求,不帶token信息,又或者是不須要登陸,不須要token便可訪問的資源。
//        String uri = request.getRequestURI();
//        for (String regexPath:permitRegexUris){
//            if (Pattern.matches(regexPath,uri)){
//                chain.doFilter(request,response);
//                return;
//            }
//        }
        if (token == null) {
            log.warn("請登陸訪問");
            throw new NoneTokenException(AuthErrorEnum.TOKEN_NEEDED.getMessage());
        }
        if (!token.startsWith(head)) {
            log.warn("token信息不合法");
            throw new IllegalTokenAuthenticationException(AuthErrorEnum.AUTH_HEADER_ERROR.getMessage());
        }
        Claims claims = JwtUtil.getClaim(token.substring(head.length()));
        if (claims == null) {
            throw new TokenIsExpiredException(AuthErrorEnum.TOKEN_EXPIRED.getMessage());
        }
        String userName = claims.getSubject();
        if (userName == null) {
            throw new TokenIsExpiredException(AuthErrorEnum.TOKEN_EXPIRED.getMessage());
        }
        Date expiredTime = claims.getExpiration();
        if ((new Date().getTime() > expiredTime.getTime())) {
            log.warn("當前token信息已過時,請從新登陸");
            throw new TokenIsExpiredException(AuthErrorEnum.TOKEN_EXPIRED.getMessage());
        }
        if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            log.info("用戶:{},正在訪問:{}", userName, request.getRequestURI());
            logger.info("authenticated user " + userName + ", setting security context");
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        }
    }
}

6.MyLogoutFilter:繼承LogoutFilter。LogoutFilter須要提供額外的兩個類,LogoutHandler和LogoutSuccessHandler。

MyLogoutHandler:實現LogoutHandler接口。

package com.example.demo.filter;

import com.example.demo.exception.IllegalTokenAuthenticationException;
import com.example.demo.exception.NoneTokenException;
import com.example.demo.util.AuthErrorEnum;
import com.example.demo.util.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class MyLogoutHandler implements LogoutHandler{

    private String tokenHeader;

    private String head;

    public MyLogoutHandler(String tokenHeader, String head) {
        this.tokenHeader = tokenHeader;
        this.head = head;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        log.info("執行登出操做...");
        String token = request.getHeader(tokenHeader);
        if (token == null) {
            log.warn("請先登陸");
            throw new NoneTokenException(AuthErrorEnum.TOKEN_NEEDED.getMessage());
        }
        if (!token.startsWith(head)){
            log.warn("token信息不合法");
            throw new IllegalTokenAuthenticationException(AuthErrorEnum.AUTH_HEADER_ERROR.getMessage());
        }
        Claims claims = JwtUtil.getClaim(token.substring(head.length()));
        if (claims == null){
            request.setAttribute("userName",null);
        }else {
            String userName = claims.getSubject();
            request.setAttribute("userName",userName);
        }
    }
}

MyLogoutSuccessHandler:實現LogoutSuccessHandler接口。

package com.example.demo.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
public class MyLogoutSuccessHandler implements LogoutSuccessHandler{

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登出成功");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write("登出成功");
    }
}

MyLogoutFilter:

package com.example.demo.filter;

import org.springframework.security.web.authentication.logout.LogoutFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
 * 默認處理登出URL爲/logout,也能夠自定義登出URL
 */
public class MyLogoutFilter extends LogoutFilter{

    public MyLogoutFilter(MyLogoutSuccessHandler logoutSuccessHandler, MyLogoutHandler logoutHandler,String filterProcessesUrl) {
        super(logoutSuccessHandler, logoutHandler);
        //更改默認的登出URL
//        super.setFilterProcessesUrl(filterProcessesUrl);
    }

    /**
     * 使用此構造方法,會使用默認的SimpleUrlLogoutSuccessHandler
     * 在登出成功後重定向到指定的logoutSuccessUrl
     * @param logoutSuccessUrl
     * @param handler
     */
    public MyLogoutFilter(String logoutSuccessUrl, MyLogoutHandler handler) {
        super(logoutSuccessUrl, handler);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        super.doFilter(req, res, chain);
    }
}

7.最後再對security配置類進行配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .cors()
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .exceptionHandling()
            .and()
            .addFilter(new MyLogoutFilter(new MyLogoutSuccessHandler(),new MyLogoutHandler(tokenHeader,head),"/logout"))
            .addFilter(new MyLoginFilter(authenticationManager(),head,tokenHeader,permissionDao))
            .addFilter(new MyAuthenticationFilter(authenticationManager(),tokenHeader,head,MyUserDetailsService()));
}

8.測試訪問其餘接口,而且登出

使用postman訪問/testApi/getData,配置requestHeader:Authorization value爲登陸時設置在response header Authorization中的token。

正常返回結果,說明token認證成功。

image.png

使用postman訪問登出接口/logout,一樣須要設置request header:具體設置token失效有不少種方法,這裏沒有給出。

image.png

6、異常處理

MyExceptionHandlerFilter,繼承OncePreRequestFilter。在這裏能夠對不一樣的異常作不一樣的處理。能夠認爲是全局異常處理,應爲該filter是在全部其餘過濾器以外,能夠捕捉到其餘過濾器和業務邏輯代碼中拋出的異常。

package com.example.demo.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 最外層filter處理驗證token、登陸認證和受權過濾器中拋出的全部異常
 */
@Slf4j
public class MyExceptionHandleFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(httpServletRequest,httpServletResponse);
        }catch (Exception e){
            log.error(e.getMessage());
            e.printStackTrace();
            httpServletResponse.setCharacterEncoding("utf-8");
            httpServletResponse.getWriter().write(e.getMessage());
        }
    }
}

在security中配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .cors()
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .exceptionHandling()
            .and()
            .addFilterBefore(new MyExceptionHandleFilter(), LogoutFilter.class)
            .addFilter(new MyLogoutFilter(new MyLogoutSuccessHandler(),new MyLogoutHandler(tokenHeader,head),"/logout"))
            .addFilter(new MyLoginFilter(authenticationManager(),head,tokenHeader,permissionDao))
            .addFilter(new MyAuthenticationFilter(authenticationManager(),tokenHeader,head,myUserDetailsService()));
}

7、處理用戶登陸後的無權訪問

MyAccessDeniedHandler,實現AccessDeniedhandler接口。能夠更細粒度進行權限控制。

package com.example.demo.filter;

import com.example.demo.util.AuthErrorEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登陸後的無權訪問在此處理
 */
@Slf4j
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        log.error("當前用戶沒有訪問該資源的權限:{}",e.getMessage());
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.getWriter().write(AuthErrorEnum.ACCESS_DENIED.getMessage());
    }
}

測試:在TestController中編寫一個接口:使用用戶「user1」登陸後,再訪問此接口,訪問成功;使用用戶「user3」登陸,再訪問此接口,返回「權限不足」。由於user1擁有permission ---p_1,user3沒有該權限。

@PreAuthorize("hasAuthority('p_1')")
@RequestMapping(value = "/authorize4",produces = MediaType.APPLICATION_JSON_VALUE)
public String authorize4(){
    return "authorized success";
}

8、解決跨域問題

 

先後端分離最可能出現的問題就是跨域問題。只須要在security配置類中配置一個CorsFilter便可。

/**
 * 解決跨域問題
 * @return
 */
@Bean
public CorsFilter corsFilter() {
    final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
    final CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.setAllowCredentials(true);
    corsConfiguration.addAllowedOrigin("*");
    corsConfiguration.addAllowedHeader("*");
    corsConfiguration.addAllowedMethod("*");
    urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
    return new CorsFilter(urlBasedCorsConfigurationSource);
}

至此,spring security + JWT的整合結束。在實際使用中,根據具體業務,能夠結合redis設置用戶綁定IP,或者同一用戶同時只能在一處登陸等功能。

9、配置公共資源或者測試時爲方便使用的接口的免登陸認真,免token的訪問

共須要在兩處配置。

1.在spring security配置類中配置。

.antMatchers(permitUris.split(",")).permitAll()

2.在MyAuthenticationFilter的doFilterInternal方法中配置,跳過無需token校驗的URL。鑑於無需驗證的URL會比較多,這裏的配置支持正則表達式匹配。把配置在配置文件中的URL轉換爲正則表達式。

String uri = request.getRequestURI();
for (String regexPath:permitRegexUris){
    if (Pattern.matches(regexPath,uri)){
        chain.doFilter(request,response);
        return;
    }
}
permitRegexUris在構造器中給出:

public MyAuthenticationFilter(AuthenticationManager authenticationManager, String tokenHeader, String head, UserDetailsService userDetailsService,String permitUris) {
    super(authenticationManager);
    this.head = head;
    this.tokenHeader = tokenHeader;
    this.userDetailsService = userDetailsService;
    this.permitRegexUris = Arrays.asList(permitUris.split(",")).stream().map(s -> {
        return PathUtil.getRegPath(s);
    }).collect(Collectors.toList());
}

完成以後,無需登陸便可訪問本身配置的資源。

10、項目結構圖

image.png

注:本文著做權歸做者,由demo大師發表,拒絕轉載,轉載須要做者受權

相關文章
相關標籤/搜索