基于SpringSecurity和JWT的用户访问认证和授权

发布时间:2018-12-03
 
技术:springsecurity+jwt+java+jpa+mysql+mysql workBench
 

概述

基于SpringSecurity和JWT的用户访问认证和授权。根据现实案例,前后端分离,并且后端为分布式部署。解决redis session共享方式的跨域问题,解决单一使用security时每次访问资源都需要用户信息进行登录的效率问题和安全问题。

详细

一、新建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,和启动项目时生成的密码 启动时日志打印的密码.png,即可登录。

二、配置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.表模型:

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 + '\'' +
                '}';
    }
}

四、创建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

至此,数据库表已经建立完成。

五、自定义关机类

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

六、异常处理

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());
}

完成之后,无需登录即可访问自己配置的资源。

十、项目结构图

image.png

注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权

猜你喜欢

转载自www.cnblogs.com/demodashi/p/10492856.html
今日推荐