1. 说明

本文主要介绍基于SpringSecurity的用户权限控制的简单实现。

1.1 环境版本

SpringBoot: 2.0.7

SpringSecurity: 5.0.10

JDK: 1.8

2. 项目配置

2.1 引入maven包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.coralcloud</groupId>
    <artifactId>security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security</name>
    <description>Demo Security project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.9</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.60</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>22.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
2.2 application.yml
server.port: 9091
spring.application.name: spring-web
spring.http.encoding.charset: utf8
spring:
  session:
    store-type: redis
  redis:
    host: localhost
    port: 6379
    password: 123456
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  servlet:
    multipart:
      max-file-size: 1024MB
      max-request-size: 1024MB

mybatis:
  mapper-locations: classpath*:mapper/*Mapper.xml
  type-aliases-package: cn.coralcloud.security.model
2.3 数据库初始化脚本
/*
Navicat MySQL Data Transfer

Source Server         : localhost
Source Server Version : 50644
Source Host           : localhost:3306
Source Database       : security

Target Server Type    : MYSQL
Target Server Version : 50644
File Encoding         : 65001

Date: 2019-12-02 16:28:44
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `nickname` varchar(50) NOT NULL,
  `system` bit(1) NOT NULL DEFAULT b'0',
  `description` varchar(500) DEFAULT NULL,
  `permission` mediumtext,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `password` varchar(300) NOT NULL,
  `role` varchar(500) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
3. 系统初始化文件
3.1 users.json
[
  {
    "username": "admin",
    "password": "ea48576f30be1669971699c09ad05c94",
    "role": "ROLE_ADMINISTRATOR"
  }
]
3.2 roles.json
[
  {
    "name": "ROLE_ADMINISTRATOR",
    "nickname": "管理员",
    "description": "系统超级管理员,不允许用户更改",
    "system": true,
    "permissions": [
      {
        "resourceId": "user",
        "resourceName": "用户管理",
        "privileges": {
          "list": "查看用户列表",
          "add": "新增用户",
          "update": "修改用户信息",
          "delete": "删除用户"
        }
      },
      {
        "resourceId": "permission",
        "resourceName": "权限",
        "privileges": {
          "read": "查看权限",
          "write": "新增权限",
          "update": "更新权限",
          "delete": "删除权限"
        }
      }
    ]
  }
]

4. 数据持久化DAO层

4.1 UserDao.java
package cn.coralcloud.security.dao;

import cn.coralcloud.security.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author geff
 * @name UserDao
 * @description
 * @date 2019-11-29 10:31
 */
@Component
@Mapper
public interface UserDao {

    /**
     * 根据用户名查找
     * @date 2019/11/29 15:24
     * @author geff
     * @param username username
     * @return cn.coralcloud.security.model.User
     */
    User findByUsername(String username);

    /**
     * 创建用户
     * @date 2019/11/29 15:24
     * @author geff
     * @param user user
     */
    void save(User user);

    /**
     * 获取用户列表
     * @date 2019/11/29 15:24
     * @author geff
     * @param
     * @return java.util.List<cn.coralcloud.security.model.User>
     */
    List<User> list();
}
4.2 RoleDao.java
package cn.coralcloud.security.dao;

import cn.coralcloud.security.model.Role;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;

/**
 * @author geff
 * @name RoleDao
 * @description
 * @date 2019-11-29 10:31
 */
@Component
@Mapper
public interface RoleDao {

    /**
     * 根据名称查找
     * @date 2019/11/29 15:23
     * @author geff
     * @param name name
     * @return cn.coralcloud.security.model.Role
     */
    Role findByName(String name);

    /**
     * 保存数据
     * @date 2019/11/29 15:23
     * @author geff
     * @param role role
     */
    void save(Role role);
}
4.3 UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.coralcloud.security.dao.UserDao" >

    <resultMap id="userMapper" type="cn.coralcloud.security.model.User">
        <id property="id" column="id" />
    </resultMap>

    <select id="findByUsername" parameterType="String" resultMap="userMapper">
        select * from `user` where `username` = #{username}
    </select>
    <select id="list" resultType="cn.coralcloud.security.model.User">
        select * from `user`
    </select>

    <insert id="save" parameterType="cn.coralcloud.security.model.User" useGeneratedKeys="true" keyProperty="id">
        insert into `user`(username, password, role)
        values (#{username}, #{password}, #{role})
    </insert>
</mapper>
4.4 RoleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.coralcloud.security.dao.RoleDao" >

    <resultMap id="roleMapper" type="cn.coralcloud.security.model.Role">
        <id property="id" column="id" />
    </resultMap>

    <select id="findByName" parameterType="String" resultMap="roleMapper">
        select * from role where `name` = #{name}
    </select>

    <insert id="save" parameterType="cn.coralcloud.security.model.Role" useGeneratedKeys="true" keyProperty="id">
        insert into role(name, nickname, description, system, permission)
        values (#{name}, #{nickname}, #{description}, #{system}, #{permission})
    </insert>
</mapper>

5. 处理用户权限认证逻辑

5.1 SpringSecurity配置

要在项目中使用@PreAuthorize等注解实现方法级别权限控制,则需要在项目启动类上添加注解@EnableGlobalMethodSecurity(prePostEnabled = true),本文项目启动类:

package cn.coralcloud.security;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

/**
 * @author geff
 */
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder applicationBuilder) {
        return applicationBuilder.sources(SecurityApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }
}
5.2 用户对象

SpringSecurity自带的有UserDetails接口主要保存用户对象数据,所以我们的用户对象需要实现UserDetails接口

package cn.coralcloud.security.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.*;

/**
 * @author geff
 */
@Data
@NoArgsConstructor
public class User implements UserDetails, Serializable {

    private Long id;

    /**
     * 用户登录名
     */
    private String username;

    /**
     * 用户登录密码,用户的密码不应该暴露给客户端
     */
    @JsonIgnore
    private String password;

    /**
     * 用户在系统中的角色列表,将根据角色对用户操作权限进行限制
     */
    private String role;

    private List<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        if(roles!= null) {
            for (Role role : roles) {
                if (role == null) {
                    continue;
                }
                for (Permission permission : role.getPermissions()) {
                    for (String privilege : permission.getPrivileges().keySet()) {
                        authorities.add(new SimpleGrantedAuthority(String.format("%s-%s", permission.getResourceId(), privilege)));
                    }
                }
            }
        }
        return authorities;
    }

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

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

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

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

}

在用户对象的public Collection<? extends GrantedAuthority> getAuthorities()方法中, 需要根据用户当前角色生成当前用户权限列表, 本文权限机制使用resourceId-privilege方式

5.3 角色对象 Role.java
package cn.coralcloud.security.model;

import com.alibaba.fastjson.JSON;
import lombok.Data;
import org.springframework.util.StringUtils;

import java.io.Serializable;
import java.util.List;

/**
 * @author geff
 * @name Role
 * @description
 * @date 2019-11-29 10:08
 */
@Data
public class Role implements Serializable {

    private Long id;

    /**
     * 角色名,用于权限校验
     */
    private String name;

    /**
     * 角色中文名,用于显示
     */
    private String nickname;

    /**
     * 角色描述信息
     */
    private String description;

    /**
     * 是否为内置
     */
    private Boolean system;

    /**
     * 角色可进行的操作列表
     */
    private List<Permission> permissions;

    private String permission;

    /**
     * Spring Security 4.0以上版本角色都默认以'ROLE_'开头
     * @param name name
     */
    public void setName(String name) {
        if (!name.contains("ROLE_")) {
            this.name = "ROLE_" + name;
        } else {
            this.name = name;
        }
    }

    public List<Permission> getPermissions(){
        if(permissions == null){
            if(!StringUtils.isEmpty(permission)){
                this.permissions = JSON.parseArray(permission, Permission.class);
            }
        }
        return permissions;
    }

    public String getPermission() {
        if(StringUtils.isEmpty(permission)){
            this.permission = JSON.toJSONString(permissions);
        }
        return permission;
    }
}

SpringSecurity4.0以上所有的角色名称默认都要以ROLE_开头,所有本文在获取角色名称是会自动添加前缀。

为了简单方便,本文角色对象的permission字段保存着该角色的所有权限列表的JSON字符串

5.4 权限对象 Permission.java
package cn.coralcloud.security.model;

import lombok.Data;

import java.io.Serializable;
import java.util.Map;

/**
 * @author geff
 * @name Permission
 * @description
 * @date 2019-11-29 10:07
 */
@Data
public class Permission implements Serializable {
    private String resourceId;

    private String resourceName;

    private Map<String, String> privileges;

    private boolean abandon = false;
}
5.5 统一数据返回对象

因为是前后端分离项目, 所以本文封装了统一数据返回对象Response类

package cn.coralcloud.security.model;

import java.io.Serializable;

/**
 * 响应
 * @author geff
 */
public class Response<T> implements Serializable {
    private final static int SUCCESS = 0;
    private final static int ERROR = -1;
    private int code;
    private T data;
    private String message;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public static <T> Response<T> ok() {
        Response<T> response = new Response<>();
        response.setCode(SUCCESS);
        return response;
    }

    public static <T> Response<T> ok(T data) {
        Response<T> response = new Response<>();
        response.setCode(SUCCESS);
        response.setData(data);
        return response;
    }

    public static <T> Response<T> fail(String message) {
        Response<T> response = new Response<>();
        response.setCode(ERROR);
        response.setMessage(message);
        return response;
    }

    public static <T> Response<T> fail(int code, String message) {
        Response<T> response = new Response<>();
        response.setCode(code);
        response.setMessage(message);
        return response;
    }
}
5.6 UserDetailsService接口获取用户信息

SpringSecurity同时在用户登录认证时会通过调用UserDetailsService的loadUserByUsername来获取当前登录的用户信息,当用户认证通过后会将用户对象保存到自定义的Token对象中。

所以本文需要实现UserDetailsService接口,完成loadUserByUsername方法:

package cn.coralcloud.security.service;

import cn.coralcloud.security.dao.RoleDao;
import cn.coralcloud.security.model.Role;
import cn.coralcloud.security.model.User;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.util.StringUtils;

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

/**
 * @author geff
 */
@Service("myUserDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleDao roleDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(String.format("No user found with username: %s", username));
        }

        if(!StringUtils.isEmpty(user.getRole())) {
            String[] roles = user.getRole().split(",");
            List<Role> roleList = new ArrayList<>();
            for (String roleName : roles) {
                Role role = roleDao.findByName(roleName);
                roleList.add(role);
            }
            user.setRoles(roleList);
        }

        return user;
    }
}
5.7 自定义方法权限处理器

自定义方法权限处理器需要实现PermissionEvaluator接口,完成public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission)方法。

package cn.coralcloud.security.config;

import cn.coralcloud.security.model.User;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.io.Serializable;

/**
 * @author geff
 */
@Configuration
public class MyPermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        boolean accessable = false;
        if(authentication.getPrincipal() instanceof User){
            String privilege = targetDomainObject + "-" + permission;
            for(GrantedAuthority authority : authentication.getAuthorities()){
                if(privilege.equalsIgnoreCase(authority.getAuthority())){
                    accessable = true;
                    break;
                }
            }

            return accessable;
        }

        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        // TODO Auto-generated method stub
        return false;
    }
}

本文通过根据resourceId-privilege方式验证权限

5.8 系统初始化配置类

本文系统初始化配置类主要实现在系统启动时根据配置的users.json和roles.json自动生成初始化用户和角色信息。

package cn.coralcloud.security.component;

import cn.coralcloud.security.dao.RoleDao;
import cn.coralcloud.security.dao.UserDao;
import cn.coralcloud.security.model.Role;
import cn.coralcloud.security.model.User;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;

import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

/**
 * 系统初始化配置类,主要用于加载内置数据到目标数据库上
 * @author geff
 */
@Component
public class SystemInitializer {

    @Value("${initialzation.file.users:users.json}")
    private String userFileName;
    @Value("${initialzation.file.roles:roles.json}")
    private String roleFileName;

    @Autowired
    private UserDao userDao;
    @Autowired
    private RoleDao roleDao;
    @Autowired
    private GsonBuilder gsonBuilder;

    @PostConstruct
    public boolean initialize() {
        try {
            InputStream userInputStream = getClass().getClassLoader().getResourceAsStream(userFileName);
            if(userInputStream == null){
                throw new Exception("initialzation user file not found: " + userFileName);
            }

            InputStream roleInputStream = getClass().getClassLoader().getResourceAsStream(roleFileName);
            if(roleInputStream == null){
                throw new Exception("initialzation role file not found: " + roleFileName);
            }

            //导入初始的系统超级管理员角色
            Type roleTokenType = new TypeToken<ArrayList<Role>>(){}.getType();
            ArrayList<Role> roles = gsonBuilder.create().fromJson(new InputStreamReader(roleInputStream, StandardCharsets.UTF_8), roleTokenType);
            for (Role role: roles) {
                if (roleDao.findByName(role.getName()) == null) {
                    roleDao.save(role);
                }
            }

            //导入初始的系统管理员用户
            Type teacherTokenType = new TypeToken<ArrayList<User>>(){}.getType();
            ArrayList<User> users = gsonBuilder.create().fromJson(new InputStreamReader(userInputStream, StandardCharsets.UTF_8), teacherTokenType);
            for (User user : users) {
                if (userDao.findByUsername(user.getUsername()) == null) {
                    userDao.save(user);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return true;
    }
}

6. 用户认证相关自定义实现

6.1. 自定义用户认证过滤器

​ 用户认证过滤器拦截用户发送的认证请求,然后从请求中获取用户账号和密码等认证信息并封装成一个未认证的AthenticationToken对象,然后调用AuthenticationManager对AthenticationToken进行认证。

​ 自定义用户认证过滤器需要继承AbstractAuthenticationProcessingFilter, 然后重写attemptAuthentication方法, 在方法内部根据请求参数封装成未认证的AthenticationToken对象

package cn.coralcloud.security.component;

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;

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

/**
 * 自定义的用户名密码认证过滤器
 * @author geff
 */
public class AuthFilter extends AbstractAuthenticationProcessingFilter {
    private static String httpMethod = "POST";
    public AuthFilter() {
        /*
         * 设置该过滤器对POST请求/api/user/login进行拦截
         */
        super(new AntPathRequestMatcher("/api/user/login", httpMethod));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals(httpMethod)) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            /*
             * 从http请求中获取用户输入的用户名和密码信息
             * 这里接收的是form形式的参数,如果要接收json形式的参数,修改这里即可
             */
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if(StringUtils.isEmpty(username) && StringUtils.isEmpty(password)) {
                throw new UsernameNotFoundException("用户名或密码错误");
            }
            /*
             * 使用用户输入的用户名和密码信息创建一个未认证的用户认证Token
             */
            AuthToken authRequest = new AuthToken(username, password);
            /*
             * 设置一些详情信息
             */
            this.setDetails(request, authRequest);
            /*
             * 通过AuthenticationManager调用相应的AuthenticationProvider进行用户认证
             */
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    private String obtainUsername(HttpServletRequest request) {
        String usernameParameter = "username";
        return request.getParameter(usernameParameter);
    }

    private String obtainPassword(HttpServletRequest request) {
        String passwordParameter = "password";
        return request.getParameter(passwordParameter);
    }

    private void setDetails(HttpServletRequest request, AuthToken authRequest) {
       authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }
}

自定义用户认证拦截器要在构造方法中指定拦截的认证请求(本文中是POST类型的/api/user/login请求,可根据需求设置),并在attemptAuthentication()方法中实现获取用户认证信息、封装AuthenticationToken对象、调用AuthenticationManager对AuthenticationToken进行认证等逻辑。

6.2 自定义用户认证处理器

用户认证处理器主要是对用户提交的认证信息进行认证,SpringSecurity默认实现的认证处理器的认证处理逻辑并不一定符合所有的业务需求(例如,默认的认证处理无法处理验证码),因此,可以自定义用户认证处理器。

自定义的用户认证处理器,需要实现AuthenticationProvider接口,主要是实现public Authentication authenticate(Authentication authentication)方法和public boolean supports(Class<?> authentication)方法,前者主要是实现具体的认证逻辑,后者主要是指定认证处理器能对哪种AuthenticationToken对象进行认证。

package cn.coralcloud.security.component;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

/**
 * @author geff
 * @name AuthProvider
 * @description
 * 登录认证的Provider,自定义实现了{@link AuthenticationProvider} <br>
 * Provider默认实现是 {@link DaoAuthenticationProvider} <br>
 * {@link UsernamePasswordAuthenticationFilter} 调用=> {@link AuthenticationManager} => {@link AuthenticationProvider}验证 <br>
 *
 * @date 2019-11-29 15:52
 */
@Slf4j
@Component
public class AuthProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailService;
    private final PasswordEncoder passwordEncoder;

    @Autowired
    public AuthProvider(@Qualifier("myUserDetailsService") UserDetailsService userDetailService, PasswordEncoder passwordEncoder) {
        this.userDetailService = userDetailService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        /*
         * 将未认证的Authentication转换成自定义的用户认证Token
         */
        AuthToken authenticationToken = (AuthToken) authentication;

        /*
         * 根据用户Token中的用户名查找用户信息,如果有该用户信息,则验证用户密码是否正确
         */
        UserDetails user = userDetailService.loadUserByUsername((String)(authenticationToken.getPrincipal()));
        if(user == null) {
            throw new BadCredentialsException("用户名或密码不正确");
        } else if(!this.passwordEncoder.matches((CharSequence) authenticationToken.getCredentials(), user.getPassword())) {
            throw new BadCredentialsException("用户名或密码不正确");
        }
        /*
         * 认证成功则创建一个已认证的用户认证Token
         */
        AuthToken authenticationResult = new AuthToken(user, user.getPassword(), user.getAuthorities());
        /*
         * 设置一些详情信息
         */
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    /**
     * 是否支持处理当前Authentication对象类似
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}
6.3 自定义用户认证对象

用户认证对象是在用户认证拦截器中创建的,在用户认证处理器中使用的。

用户认证对象(AuthenticationToken)中封装的是用户认证信息,例如UsernamePasswordAuthenticationToken中封装的是用户名和密码。实际业务中,可能需要根据不同的用户信息进行认证(例如,手机号和验证码),此时就需要自定义用户认证对象。

自定义的用户认证对象,需要继承AbstractAuthenticationToken类,并设定根据认证时使用的是哪些信息。

package cn.coralcloud.security.component;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;

/**
 * 自定义的用户名密码认证对象
 * @author geff
 */
public class AuthToken extends AbstractAuthenticationToken {
    /**
     * 用户名
     */
    private final Object principal;

    /**
     * 密码
     */
    private Object credentials;

    /**
     * 创建未认证的用户名密码认证对象
     */
    public AuthToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    /**
     * 创建已认证的用户密码认证对象
     */
    public AuthToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }

}
6.4 自定义用户认证成功处理器

用户认证成功处理器在用户认证成功之后调用,主要是执行一些额外的操作(例如,操作Cookie、页面跳转等)。

自定义的用户认证成功处理器可以通过实现AuthenticationSuccessHandler接口,或者通过继承AbstractAuthenticationTargetUrlRequestHandler类及其子类来实现。本文自定义的用户认证成功处理器是通过继承AbstractAuthenticationTargetUrlRequestHandler的子类SavedRequestAwareAuthenticationSuccessHandler来实现的。

package cn.coralcloud.security.component;

import cn.coralcloud.security.model.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * 自定义的用户认证成功处理器
 * @author geff
 */
@Component
@Slf4j
public class AuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    public AuthSuccessHandler() {

    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        // 认证成功返回json
        User user = (User)authentication.getPrincipal();
        // 写入session?
        HttpSession session = request.getSession();
        session.setAttribute("User", user);
        String jsonStr = objectMapper.writeValueAsString(user);
        log.info("认证成功: {}", jsonStr);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(jsonStr);
    }
}
6.5 自定义用户认证失败处理器

用户认证失败处理器是在用户认证失败之后调用,主要是执行一些额外的操作(例如操作Cookie、页面跳转、返回错误信息等)。
自定义的用户认证失败处理器可以通过实现AuthenticationFailureHandler接口,或者通过继承AuthenticationFailureHandler接口的其它实现类来实现。本文自定义的用户认证失败处理器是通过继承AuthenticationFailureHandler接口的实现类SimpleUrlAuthenticationFailureHandler来实现的。

package cn.coralcloud.security.component;

import cn.coralcloud.security.model.Response;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

/**
 * 自定义的用户认证失败处理器
 * @author geff
 */
@Component
@Slf4j
public class AuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        /*
         * 返回JSON
         */
        log.error("认证失败: {}", exception.getMessage());
        Response res = Response.fail(-1, exception.getMessage());
        response.setStatus(HttpStatus.OK.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(res));
        response.getWriter().flush();
    }

}
6.6 自定义访问拒绝处理器

自定义访问拒绝处理器用来解决认证过的用户访问无权限资源时的异常。

前后端分离的情况下可以通过自定义访问拒绝处理器实现JSON格式的数据返回,自定义访问拒绝处理器通过实现AccessDeniedHandler接口,然后实现public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)方法,在方法内部处理返回数据。

package cn.coralcloud.security.component;

import cn.coralcloud.security.model.Response;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * @author geff
 * @name AuthAccessDeniedHandler
 * @description
 * @date 2019-11-29 16:57
 */
@Slf4j
@Component
public class AuthAccessDeniedHandler implements AccessDeniedHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
        log.error("无权访问: {}", e.getMessage());
        Response res = Response.fail(401, "无权访问");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(res));
        response.getWriter().flush();
    }
}
6.7 自定义加密类

本文密码使用Md5(password, salt)的形式, 所以需要自定义SpringSecurity加密类, 然后再Config配置类注入

自定义加密类需要实现PasswordEncoder,完成encode和matches方法

package cn.coralcloud.security.component;

import cn.coralcloud.security.utils.SecretUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.StringUtils;

import java.util.Objects;

/**
 * @author geff
 * @name Md5SaltPasswordEncoder
 * @description
 * @date 2019-12-02 09:27
 */
public class Md5SaltPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        String string = charSequence.toString();
        String[] array = string.split(",");
        String salt = "";
        if(array.length > 1){
            salt = array[1];
        }
        return SecretUtils.md5(array[0], salt);
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        if(!StringUtils.isEmpty(s)){
            String encodePassword = encode(charSequence);
            return Objects.equals(encodePassword, s);
        }

        return false;
    }
}

7. SpringSecurity相关配置类

7.1 自定义的用户名密码认证配置类
package cn.coralcloud.security.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

/**
 * 自定义的用户名密码认证配置类
 * @author geff
 */
@Component
public class AuthConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    AuthProvider provider;

    @Autowired
    AuthSuccessHandler authSuccessHandler;

    @Autowired
    AuthFailureHandler authFailureHandler;

    @Override
    public void configure(HttpSecurity http) {
        AuthFilter authFilter = new AuthFilter();
        /*
         * 自定义用户认证处理逻辑时,需要指定AuthenticationManager,否则无法认证
         */
        authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        /*
         * 指定自定义的认证成功和失败的处理器
         */
        authFilter.setAuthenticationSuccessHandler(authSuccessHandler);
        authFilter.setAuthenticationFailureHandler(authFailureHandler);

        /*
         * 把自定义的用户名密码认证过滤器和处理器添加到UsernamePasswordAuthenticationFilter过滤器之前
         */
        http.authenticationProvider(provider)
            .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
7.2 SpringSecurity核心配置类
package cn.coralcloud.security.config;

import cn.coralcloud.security.component.AuthAccessDeniedHandler;
import cn.coralcloud.security.component.AuthConfig;
import cn.coralcloud.security.component.Md5SaltPasswordEncoder;
import cn.coralcloud.security.model.Response;
import cn.coralcloud.security.service.UserDetailsServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.io.PrintWriter;

/**
 * @author geff
 */
@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthConfig authConfig;
    @Autowired
    private AuthAccessDeniedHandler accessDeniedHandler;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.apply(authConfig)
            .and()
            .authorizeRequests()
            .antMatchers("/static/**", "/api/user/login").permitAll().anyRequest().authenticated()
            .and().csrf().disable();
        httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
        .authenticationEntryPoint((request, response, e) -> {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            Response res = Response.fail(-14, "会话超时, 请重新登录!");
            out.write(new ObjectMapper().writeValueAsString(res));
            out.flush();
            out.close();
        })
        ;
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new Md5SaltPasswordEncoder();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }
}

本文通过自定义AuthenticationEntryPoint来解决匿名用户访问无权限资源时的异常