Skip to content

::: note

本内容介绍 Spring Security 与 JWT 的简单应用,一些工具类也是实际开发生产使用的的类。 :::

简单介绍

Spring Security 在 Web 编程开发中,登录安全往往也是很重要的一个部分,而 Spring Security 所做得就是这个工作。在 java 领域,成熟的安全框架解决方案一般有 Apache Shiro、Spring Security 等两种技术选型。Apache Shiro 简单易用也算是一大优势,但其功能还是远不如 Spring Security 强大。后者可以为应用提供声明式的安全访问限制,他提供了一系列的可以由开发者主动配置的 bean ,并利用 Spring IoC和 AOP等功能特性来为应用系统提供声明式的安全访问控制功能,减少了诸多重复工作。

JWT 的全称是:Json Web Token 。是在网路应用中传递信息的一种基于 json 的开发标准,可用于作为 json 对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在身份提供者和服务提供者间传递被认证的用户身份信息。

数据库准备

本内容因为只是简单的实战,所以只有常用的三个表:用户表 sys_user,权限表 sys_role,两者关联表 user_role

sql
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `roleName` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

LOCK TABLES `sys_role` WRITE;
INSERT INTO `sys_role` VALUES (1,'ROLE_NORMAL'),(2,'ROLE_ADMIN');
UNLOCK TABLES;

DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

LOCK TABLES `sys_user` WRITE;
INSERT INTO `sys_user` VALUES (1,'kele','$2a$10$mu4mzB48PJ9r3TZJDN5f9.nvM3LOSj2.D8cpriU5ZeW29S1.VJ3lm');
UNLOCK TABLES;

DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int DEFAULT NULL,
  `role_id` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

LOCK TABLES `user_role` WRITE;
INSERT INTO `user_role` VALUES (1,1,1),(2,1,2);
UNLOCK TABLES;

效果如图:

image-20211224192105286

密码采用了加密方式,下面会有加密的方式类。未加密的密码为 kele1234

目录结构

先预览项目的目录结构

md
包名 cn.kbt
├── bean (数据库对应的实体类包)
│   ├── Role
│   ├── User
├── common (响应封装类)
│   ├── HttpResult
│   ├── HttpStatus
├── config (Security 配置类)
│   ├── WebSecurityConfig
├── controller 
│   ├── JwtAuthController
│   ├── JwtTestController
├── mapper 
│   ├── UserMapper
├── security (Security 实现类)
│   ├── JwtAuthenticationFilter
│   ├── JwtAuthenticationToken
│   ├── JwtUserDetails
│   ├── UserDetailsServiceImpl
├── service 
│   ├── impl
│		└── UserServiceImpl
│   ├── UserService
├── util (JWT 和 Security 工具类)
│   ├── JwtTokenUtils
│   ├── SecurityUtils

└── SpringSecurityApplication

resources 目录结构:

md
resources
├── mapper 
│   ├── UserMapper.xml

└── application.yml

项目准备

  • 创建一个 Spring Boot 项目

  • 添加依赖,如下:

    xml
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.5.5</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>cn.kbt</groupId>
        <artifactId>my-security</artifactId>
        <version>1.0</version>
    
        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.2.0</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <!-- Spring Security -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <!-- jwt -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.1</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
  • 添加 application.yml 配置文件,添加如下内容:

    yml
    server:
      port: 7272
    
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/securitydb?useSSL-=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: 123456
    mybatis:
      mapper-locations: classpath:mapper/*Mapper.xml
      configuration:
        call-setters-on-nulls: true
    
    logging:
      level:
        cn.kbt.mapper: DEBUG

    17 行代码是 mapper 接口所在的包名,这样触发 sql 就会打印在控制台上。

  • 创建实体类,要求与数据库的表对应

    User.java:(记得加上 set 和 get 方法)

    java
    /**
     * @author xx
     * @date 2021/12/24 17:18
     * @description 用户类,与数据库的表对应
     */
    public class User {
        private Long id;
    
        private String username;
    
        private String password;
    
        private List<Role> roles;
    
        public User() {
        }
    
        // set get ......
    }

    Role.java:(记得加上 set 和 get 方法)

    java
    /**
     * @author xx
     * @date 2021/12/24 16:25
     * @description 权限类,与数据库的表对应
     */
    public class Role {
        private Long id;
        
        private String roleName;
    
        public Role() {
        }
    
    	// set get ......
    }
  • 编写 mapper 接口和 xml 文件,以及 server 接口和 server 实现类

    UserMapper.java

    java
    @Mapper
    public interface UserMapper {
        User findByUsername(String username);
    
        List<Role> findPermissions(String username);
    }

    UserMapper.xml

    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.kbt.mapper.UserMapper">
    
        <select id="findByUsername" resultType="cn.kbt.bean.User">
            select * from sys_user where username = #{username}
        </select>
    
        <select id="findPermissions" resultType="cn.kbt.bean.Role">
            select r.* from sys_user u,sys_role r,user_role ur where u.id = ur.user_id and r.id = ur.role_id and username = #{username}
        </select>
    </mapper>

    UserService.java

    java
    /**
     * @author xx
     * @date 2021/12/24 16:07
     * @description 用户 server 接口
     */
    public interface UserService {
    
        User findByUsername(String username);
    
        List<Role> findPermissions(String username);
    }

    UserServiceImpl.java

    java
    /**
     * @author xx
     * @date 2021/12/24 16:08
     * @description 用户 server 实现类
     */
    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public User findByUsername(String username) {
            return userMapper.findByUsername(username);
        }
    
        @Override
        public List<Role> findPermissions(String username) {
            return userMapper.findPermissions(username);
        }
    }

JWT和Security工具类

JwtTokenUtils 类封装了 JWT 的各种操作,包括生成 token,解析 token,刷新 token,判断 token 是否过期等操作,是一个通用的类

java
package cn.kbt.util;

import cn.kbt.security.JwtAuthenticationToken;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.*;

/**
 * @author xx
 * @date 2021/12/24 15:49
 * @description JWT 工具类
 */
public class JwtTokenUtils implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户名称,可自定义,或者获取官方提供
     */
    private static final String USERNAME = Claims.SUBJECT;
    /**
     * 创建时间
     */
    private static final String CREATED = "created";
    /**
     * 权限列表
     */
    private static final String AUTHORITIES = "authorities";
    /**
     * 密钥,自定义,根据密钥生成 token,或还原 token
     */
    private static final String SECRET = "abcdefgh";
    /**
     * 有效期 12 小时
     */
    private static final long EXPIRE_TIME = 12 * 60 * 60 * 1000;

    /**
     * 生成令牌
     * @param authentication 认证信息
     * @return 令牌
     */
    public static String generateToken(Authentication authentication) {
        Map<String, Object> claims = new HashMap<>(3);
        claims.put(USERNAME, SecurityUtils.getUsername(authentication));
        claims.put(CREATED, new Date());
        claims.put(AUTHORITIES, authentication.getAuthorities());
        return generateToken(claims);
    }

    /**
     * 从数据声明生成令牌
     * @param claims 数据声明
     * @return 令牌
     */
    private static String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
    }

    /**
     * 从令牌中获取用户名
     * @param token 令牌
     * @return 用户名
     */
    public static String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 根据请求令牌获取登录认证信息
     * @param request 客户端的请求
     * @return 用户名
     */
    public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
        Authentication authentication = null;
        // 获取请求携带的令牌
        String token = JwtTokenUtils.getToken(request);
        // 如果请求令牌不为空
        if (token != null) {
            // 如果在 Security 上下文检测没有登录过
            if (SecurityUtils.getAuthentication() == null) {
                // 根据 token 获取曾经登录的数据证明
                Claims claims = getClaimsFromToken(token);
                if (claims == null) {
                    return null;
                }
                String username = claims.getSubject();
                if (username == null) {
                    return null;
                }
                // 如果 token 过期
                if (isTokenExpired(token)) {
                    return null;
                }
                // 获取用户的权限列表
                Object authors = claims.get(AUTHORITIES);
                List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
                // 如果用户权限是集合,则存入新的集合里
                if (authors != null && authors instanceof List) {
                    for (Object object : (List) authors) {
                        authorities.add(new SimpleGrantedAuthority((String) ((Map) object).get("authority")));
                    }
                }
                authentication = new JwtAuthenticationToken(username, null, authorities, token);
            } else {
                // 如果上下文有用户登录过,则检查是否是当前用户
                if (validateToken(token, SecurityUtils.getUsername())) {
                    // 如果上下文中 Authentication 非空,且请求令牌合法,直接返回当前登录认证信息
                    authentication = SecurityUtils.getAuthentication();
                }
            }
        }
        return authentication;
    }

    /**
     * 从令牌中获取数据声明
     * @param token 令牌
     * @return 数据声明
     */
    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 验证令牌
     * @param token 令牌
     * @param username 用户名
     * @return 令牌是否正确
     */
    public static Boolean validateToken(String token, String username) {
        String userName = getUsernameFromToken(token);
        return (userName.equals(username) && !isTokenExpired(token));
    }

    /**
     * 刷新令牌
     * @param token 令牌
     * @return 新的令牌
     */
    public static String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put(CREATED, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 判断令牌是否过期
     * @param token 令牌
     * @return 是否过期
     */
    public static Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 根据请求获取 token
     * @param request 用户请求
     * @return 令牌
     */
    public static String getToken(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        String tokenHead = "Bearer ";
        if (token == null) {
            token = request.getHeader("token");
        } else if (token.contains(tokenHead)) {
            // 把 Bearer 去掉,只要后面的 token 值
            token = token.substring(tokenHead.length());
        }
        if ("".equals(token)) {
            token = null;
        }
        return token;
    }
}

SecurityUtils 类封装了 Spring Security 相关的操作,如认证 token,登录验证,获取登录信息等操作,也是一个通用的类。

java
package cn.kbt.util;

import cn.kbt.security.JwtAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

import javax.servlet.http.HttpServletRequest;

/**
 * @author xx
 * @date 2021/12/24 15:50
 * @description
 */
public class SecurityUtils {

    /**
     * 系统登录认证
     * @param request 客户端请求
     * @param username 当前用户名
     * @param password 当前用户密码
     * @param authenticationManager 认证对象
     * @return 认证后的用户信息,内容包括 token
     */
    public static JwtAuthenticationToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
        JwtAuthenticationToken token = new JwtAuthenticationToken(username, password);
        // token 存入 request 的相关信息
        token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        // 执行登录认证过程,获取调用 UserDetailsServiceImpl 的 loadUserByUsername 方法
        Authentication authentication = authenticationManager.authenticate(token);
        // 认证成功存储认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 生成令牌并返回给客户端 
        token.setToken(JwtTokenUtils.generateToken(authentication));
        return token;
    }

    /**
     * 获取令牌进行认证
     * @param request 客户端请求
     */
    public static void checkAuthentication(HttpServletRequest request) {
        // 获取令牌并根据令牌获取登录认证信息
        Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
        // 设置登录认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    /**
     * 从上下文获取当前用户名
     * @return 当前用户名
     */
    public static String getUsername() {
        String username = null;
        Authentication authentication = getAuthentication();
        if (authentication != null) {
            Object principal = authentication.getPrincipal();
            if (principal != null && principal instanceof UserDetails) {
                username = ((UserDetails) principal).getUsername();
            } else {
                return String.valueOf(principal);
            }
        }
        return username;
    }

    /**
     * 从传入的认证信息中获取用户名
     * @return 用户名
     */
    public static String getUsername(Authentication authentication) {
        String username = null;
        if (authentication != null) {
            Object principal = authentication.getPrincipal();
            if (principal != null && principal instanceof UserDetails) {
                username = ((UserDetails) principal).getUsername();
            } else {
                return String.valueOf(principal);
            }
        }
        return username;
    }

    /**
     * 从上下文获取当前登录信息
     * @return 当前登录信息
     */
    public static Authentication getAuthentication() {
        if (SecurityContextHolder.getContext() == null) {
            return null;
        }
        return SecurityContextHolder.getContext().getAuthentication();
    }
}

Security层实现类

本内容全是涉及了 Spring Security 的操作,也封装了一些 Security 需要的实体类,以及自动验证权限类。

首先是拦截器类,因为将用户的权限交给 Spring Security 管理,则需要提供一个拦截器,专门拦截没有认证的用户信息

java
package cn.kbt.security;

import cn.kbt.util.SecurityUtils;
import org.springframework.security.authentication.AuthenticationManager;
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;

/**
 * @author xx
 * @date 2021/12/24 16:29
 * @description 用户访问项目的资源前,先进行验证 token
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 访问项目前,获取 request 的 token, 并检查登录状态
        SecurityUtils.checkAuthentication(request);
        // 释放目标资源到其他拦截器
        chain.doFilter(request, response);
    }
}

其次是一个用户权限信息的封装类,该类继承 UsernamePasswordAuthenticationToken 类,如果看过源码,就会发现继承类提供了两个属性:用户的用户名和密码,但这是官方提供的类,而因为我需要将用户的用户名和密码以及 token 一起封装返回给客户端,所以我自定义一个类,继承 UsernamePasswordAuthenticationToken ,不仅实现了官方自带的用户名和密码属性,还加了 token 属性

java
package cn.kbt.security;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * @author xx
 * @date 2021/12/24 15:55
 * @description 相比较自带的 UsernamePasswordAuthenticationToken 类,多了 token 属性
 *              这个类用于返回给客户端,包含用户的用户名、密码、token
 */
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {

    private static final long serialVersionUID = 1L;

    private String token;

    public JwtAuthenticationToken(Object principal, Object credentials){
        super(principal, credentials);
    }

    public JwtAuthenticationToken(Object principal, Object credentials, String token){
        super(principal, credentials);
        this.token = token;
    }

    public JwtAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
        super(principal, credentials, authorities);
        this.token = token;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public static long getSerialversionuid() {
        return serialVersionUID;
    }
}

然后我们需要提供一个 User 类给 Spring Security 来封装我们的用户名、密码、以及 多个权限,但是这个 User 类不能与数据库对应的实体类 User 一样,因为两者的目标不一样,不能混为一体使用

java
package cn.kbt.security;

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

import java.util.Collection;

/**
 * @author xx
 * @date 2021/12/24 15:48
 * @description 继承 UserDetails 类,代表 Security 认证的用户
 */

public class JwtUserDetails implements UserDetails {

    private String username;

    private String password;
    /**
     * 用户的权限
     */
    private Collection<? extends GrantedAuthority> authorities;

    public JwtUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public String getPassword() {
        return password;
    }

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

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

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

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

最后是非常重要的类,该类决定了当用户访问项目资源时,Spring Security 如何进行用户权限认证

java
package cn.kbt.security;

import cn.kbt.bean.Role;
import cn.kbt.bean.User;
import cn.kbt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.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;

/**
 * @author xx
 * @date 2021/12/24 16:06
 * @description 该类是 Spring Security 自动调用,判断用户是否存在、有权限
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("该用户不存在");
        }
        // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
        List<Role> permissions = userService.findPermissions(username);
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        for (Role permission : permissions) {
            grantedAuthorities.add(new SimpleGrantedAuthority(permission.getRoleName()));
        }
        return new JwtUserDetails(user.getUsername(), user.getPassword(), grantedAuthorities);
    }
}

Security配置类

这个类很重要,因为上面的所有类目前都是白写,因为没有将他们使用起来,而配置类,就是将所有相关的类,尽数连接、使用起来

java
package cn.kbt.config;

import cn.kbt.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;

/**
 * @author xx
 * @date 2021/12/24 17:26
 * @description Spring Security 配置
 */
@Configuration
@EnableWebSecurity    // 开启 Spring Security
@EnableGlobalMethodSecurity(prePostEnabled = true)    // 开启权限注解,如:@PreAuthorize注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 启用加密方式
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用 csrf, 由于使用的是 JWT,我们这里不需要 csrf
        http.cors().and().csrf().disable()
            .authorizeRequests()
            // 跨域预检请求
            .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
            // 首页和登录页面放行
            .antMatchers("/").permitAll()
            .antMatchers("/login").permitAll()
            // 其他所有请求需要身份认证
            .anyRequest().authenticated();
        http.headers().frameOptions().disable();
        // 退出登录处理器
        http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
        // token 验证过滤器
        http.addFilterBefore(new JwtAuthenticationFilter(super.authenticationManager()), UsernamePasswordAuthenticationFilter.class);
    }
    /**
     * 加密类可以被 Autowired
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

响应封装类

这一封装类也是常用的类,基本项目都会使用该类

java
package cn.kbt.common;


/**
 * @author xx
 * @date 2021/12/24 16:41
 * @description
 */
public class HttpResult {

    private int code = 200;
    private String msg;
    private Object data;

    public static HttpResult error() {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常!");
    }

    public static HttpResult error(String msg) {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    }

    public static HttpResult error(int code, String msg) {
        HttpResult r = new HttpResult();
        r.setCode(code);
        r.setMsg(msg);
        return r;
    }

    public static HttpResult ok(String msg) {
        HttpResult r = new HttpResult();
        r.setMsg(msg);
        return r;
    }

    public static HttpResult ok(Object data) {
        HttpResult r = new HttpResult();
        r.setData(data);
        return r;
    }

    public static HttpResult ok() {
        return new HttpResult();
    }

    public int getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

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

响应返回的状态类如下:

java
package cn.kbt.common;

/**
 * Constants enumerating the HTTP status codes.
 * All status codes defined in RFC1945 (HTTP/1.0), RFC2616 (HTTP/1.1), and
 * RFC2518 (WebDAV) are listed.
 *
 *
 * @since 4.0
 */
public interface HttpStatus {

    // --- 1xx Informational ---

    /** {@code 100 Continue} (HTTP/1.1 - RFC 2616) */
    public static final int SC_CONTINUE = 100;
    /** {@code 101 Switching Protocols} (HTTP/1.1 - RFC 2616)*/
    public static final int SC_SWITCHING_PROTOCOLS = 101;
    /** {@code 102 Processing} (WebDAV - RFC 2518) */
    public static final int SC_PROCESSING = 102;

    // --- 2xx Success ---

    /** {@code 200 OK} (HTTP/1.0 - RFC 1945) */
    public static final int SC_OK = 200;
    /** {@code 201 Created} (HTTP/1.0 - RFC 1945) */
    public static final int SC_CREATED = 201;
    /** {@code 202 Accepted} (HTTP/1.0 - RFC 1945) */
    public static final int SC_ACCEPTED = 202;
    /** {@code 203 Non Authoritative Information} (HTTP/1.1 - RFC 2616) */
    public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
    /** {@code 204 No Content} (HTTP/1.0 - RFC 1945) */
    public static final int SC_NO_CONTENT = 204;
    /** {@code 205 Reset Content} (HTTP/1.1 - RFC 2616) */
    public static final int SC_RESET_CONTENT = 205;
    /** {@code 206 Partial Content} (HTTP/1.1 - RFC 2616) */
    public static final int SC_PARTIAL_CONTENT = 206;
    /**
     * {@code 207 Multi-Status} (WebDAV - RFC 2518)
     * or
     * {@code 207 Partial Update OK} (HTTP/1.1 - draft-ietf-http-v11-spec-rev-01?)
     */
    public static final int SC_MULTI_STATUS = 207;

    // --- 3xx Redirection ---

    /** {@code 300 Mutliple Choices} (HTTP/1.1 - RFC 2616) */
    public static final int SC_MULTIPLE_CHOICES = 300;
    /** {@code 301 Moved Permanently} (HTTP/1.0 - RFC 1945) */
    public static final int SC_MOVED_PERMANENTLY = 301;
    /** {@code 302 Moved Temporarily} (Sometimes {@code Found}) (HTTP/1.0 - RFC 1945) */
    public static final int SC_MOVED_TEMPORARILY = 302;
    /** {@code 303 See Other} (HTTP/1.1 - RFC 2616) */
    public static final int SC_SEE_OTHER = 303;
    /** {@code 304 Not Modified} (HTTP/1.0 - RFC 1945) */
    public static final int SC_NOT_MODIFIED = 304;
    /** {@code 305 Use Proxy} (HTTP/1.1 - RFC 2616) */
    public static final int SC_USE_PROXY = 305;
    /** {@code 307 Temporary Redirect} (HTTP/1.1 - RFC 2616) */
    public static final int SC_TEMPORARY_REDIRECT = 307;

    // --- 4xx Client Error ---

    /** {@code 400 Bad Request} (HTTP/1.1 - RFC 2616) */
    public static final int SC_BAD_REQUEST = 400;
    /** {@code 401 Unauthorized} (HTTP/1.0 - RFC 1945) */
    public static final int SC_UNAUTHORIZED = 401;
    /** {@code 402 Payment Required} (HTTP/1.1 - RFC 2616) */
    public static final int SC_PAYMENT_REQUIRED = 402;
    /** {@code 403 Forbidden} (HTTP/1.0 - RFC 1945) */
    public static final int SC_FORBIDDEN = 403;
    /** {@code 404 Not Found} (HTTP/1.0 - RFC 1945) */
    public static final int SC_NOT_FOUND = 404;
    /** {@code 405 Method Not Allowed} (HTTP/1.1 - RFC 2616) */
    public static final int SC_METHOD_NOT_ALLOWED = 405;
    /** {@code 406 Not Acceptable} (HTTP/1.1 - RFC 2616) */
    public static final int SC_NOT_ACCEPTABLE = 406;
    /** {@code 407 Proxy Authentication Required} (HTTP/1.1 - RFC 2616)*/
    public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
    /** {@code 408 Request Timeout} (HTTP/1.1 - RFC 2616) */
    public static final int SC_REQUEST_TIMEOUT = 408;
    /** {@code 409 Conflict} (HTTP/1.1 - RFC 2616) */
    public static final int SC_CONFLICT = 409;
    /** {@code 410 Gone} (HTTP/1.1 - RFC 2616) */
    public static final int SC_GONE = 410;
    /** {@code 411 Length Required} (HTTP/1.1 - RFC 2616) */
    public static final int SC_LENGTH_REQUIRED = 411;
    /** {@code 412 Precondition Failed} (HTTP/1.1 - RFC 2616) */
    public static final int SC_PRECONDITION_FAILED = 412;
    /** {@code 413 Request Entity Too Large} (HTTP/1.1 - RFC 2616) */
    public static final int SC_REQUEST_TOO_LONG = 413;
    /** {@code 414 Request-URI Too Long} (HTTP/1.1 - RFC 2616) */
    public static final int SC_REQUEST_URI_TOO_LONG = 414;
    /** {@code 415 Unsupported Media Type} (HTTP/1.1 - RFC 2616) */
    public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
    /** {@code 416 Requested Range Not Satisfiable} (HTTP/1.1 - RFC 2616) */
    public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
    /** {@code 417 Expectation Failed} (HTTP/1.1 - RFC 2616) */
    public static final int SC_EXPECTATION_FAILED = 417;

    /**
     * Static constant for a 418 error.
     * {@code 418 Unprocessable Entity} (WebDAV drafts?)
     * or {@code 418 Reauthentication Required} (HTTP/1.1 drafts?)
     */
    // not used
    // public static final int SC_UNPROCESSABLE_ENTITY = 418;

    /**
     * Static constant for a 419 error.
     * {@code 419 Insufficient Space on Resource}
     * (WebDAV - draft-ietf-webdav-protocol-05?)
     * or {@code 419 Proxy Reauthentication Required}
     * (HTTP/1.1 drafts?)
     */
    public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419;
    /**
     * Static constant for a 420 error.
     * {@code 420 Method Failure}
     * (WebDAV - draft-ietf-webdav-protocol-05?)
     */
    public static final int SC_METHOD_FAILURE = 420;
    /** {@code 422 Unprocessable Entity} (WebDAV - RFC 2518) */
    public static final int SC_UNPROCESSABLE_ENTITY = 422;
    /** {@code 423 Locked} (WebDAV - RFC 2518) */
    public static final int SC_LOCKED = 423;
    /** {@code 424 Failed Dependency} (WebDAV - RFC 2518) */
    public static final int SC_FAILED_DEPENDENCY = 424;

    // --- 5xx Server Error ---

    /** {@code 500 Server Error} (HTTP/1.0 - RFC 1945) */
    public static final int SC_INTERNAL_SERVER_ERROR = 500;
    /** {@code 501 Not Implemented} (HTTP/1.0 - RFC 1945) */
    public static final int SC_NOT_IMPLEMENTED = 501;
    /** {@code 502 Bad Gateway} (HTTP/1.0 - RFC 1945) */
    public static final int SC_BAD_GATEWAY = 502;
    /** {@code 503 Service Unavailable} (HTTP/1.0 - RFC 1945) */
    public static final int SC_SERVICE_UNAVAILABLE = 503;
    /** {@code 504 Gateway Timeout} (HTTP/1.1 - RFC 2616) */
    public static final int SC_GATEWAY_TIMEOUT = 504;
    /** {@code 505 HTTP Version Not Supported} (HTTP/1.1 - RFC 2616) */
    public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;

    /** {@code 507 Insufficient Storage} (WebDAV - RFC 2518) */
    public static final int SC_INSUFFICIENT_STORAGE = 507;
}

Controller类

写好了所有的配置后,我们现在只需要进行 Controller 层的配置,因为只是测试,所以非常简单。

首先是登录认证 Controller 类

java
/**
 * @author xx
 * @date 2021/12/24 16:26
 * @description JWT 认证 controller
 */
@RestController
public class JwtAuthController {

    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    
    @PostMapping("/login")
    public HttpResult login(@RequestParam("username") String username, @RequestParam("password") String password, HttpServletRequest request) {
        User user = userService.findByUsername(username);
        // 账号不存在
        if (user == null) {
            return HttpResult.error("账号不存在");
        // 数据库的密码是加密过的
        }else if(!bCryptPasswordEncoder.matches(password,user.getPassword())) {
            return HttpResult.error("密码不正确");
        }
        
        // 该工具类是进行权限认证,将用户存入 Security 里
        JwtAuthenticationToken token = SecurityUtils.login(request, username, password, authenticationManager);
        return HttpResult.ok(token);
    }   
}

然后是测试 Controller 类

java
/**
 * @author xx
 * @date 2021/12/24 17:43
 * @description JWT 测试 controller
 */
@RestController
public class JwtTestController {
    // 测试普通权限
    @PreAuthorize("hasAuthority('ROLE_NORMAL')")
    @GetMapping( value="/normal/test")
    public String test1() {
        return "ROLE_NORMAL /normal/test 接口调用成功!";
    }

    // 测试管理员权限
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @GetMapping( value = "/admin/test")
    public String test2() {
        return "ROLE_ADMIN /admin/test 接口调用成功!";
    }
    
}

启动类

最后是启动类,也就是项目的入口类

java
/**
 * @author xx
 * @date 2021/12/24 17:34
 * @description 启动类
 */
@SpringBootApplication
@MapperScan("cn.kbt.mapper")
public class SpringSecurityApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityApplication.class, args);
    }   
}

最终测试

启动项目,因为登录是使用 POST 请求,所以我们使用 Postman 软件进行测试

首先我们进行登录,确保用户名和密码正确,然后成功后会返回 token,请记住它,它将是你访问项目的凭证

image-20211224235439860

接着我们直接访问测试 Controller 类,会发现无法访问,因为并没有携带 token

image-20211224235610983

最后我们在 请求头 携带 token,重新访问,发现能成功访问,说明权限验证通过

image-20211224235804469