概述
詳細
一、新建springboot項目
1.配置數據庫連接。 確保項目成功運行,並能訪問。
2.引入springsecurity依賴和JWT依賴。
<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的默認登錄界面。也可以使用自己的登錄頁面。
使用默認的用戶名:user,和啟動項目時生成的密碼
,即可登錄。
二、配置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、設置權限等安全控制。
@EnableGlobaleMethodSecurity: Spring security默認是禁用注解的,要想開啟注解,需要繼承WebSecurityConfigurerAdapter的類上加@EnableGlobalMethodSecurity注解,來判斷用戶對某個控制層的方法是否具有訪問權限。
①開啟@Secured注解過濾權限: @EnableGlobalMethodSecurity(securedEnabled=true)
@Secured:認證是否有權限訪問。eg:
@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):
三、創建RBAC模型中 數據庫表的模型
使用mysql workBench 或者 Power Design 創建表關系物理模型導出為SQL腳本,再執行SQL腳本即可創建,或者直接編寫DDL語句。
也可以使用JPA通過在實體上使用注解,在程序啟動時自動生成表和設置表關系。(配置spring.jpa.ddl-auto=update)。
------------------------方便理清表結構和關系,這里使用了mysql workBench 繪制了表結構關系物理模型----------------------------
1.表模型:

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,生成表。

-------------------------------------------------------使用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 + '\'' +
'}';
}
}
四、創建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腳本,重新執行即可。
在Navicat for mysql中的設置方法:選擇存在外鍵的表,設計表,外鍵。

至此,數據庫表已經建立完成。
五、自定義關機類
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頒發已經全部完成。詳細流程如下圖:

使用postman測試登錄接口:

返回:


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認證成功。

使用postman訪問登出接口/logout,同樣需要設置request header:具體設置token失效有很多種方法,這里沒有給出。

六、異常處理
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()));
}
七、處理用戶登錄后的無權訪問
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";
}
八、解決跨域問題
前后端分離最可能出現的問題就是跨域問題。只需要在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,或者同一用戶同時只能在一處登錄等功能。
九、配置公共資源或者測試時為方便使用的接口的免登錄認真,免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());
}
完成之后,無需登錄即可訪問自己配置的資源。
十、項目結構圖

