SpringSecurity前后端分离

SpringSecurity前后端分离(动态鉴权)

一、认证流程讲解

1、原始认证流程

原始认证流程通常会配合Session一起使用,但前后端分离后就用不到Session了

SpringSecurity默认的认证流程如下图(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)

SpringSecurity前后端分离_第1张图片

DaoAuthenticationProvider继承AbstractUserDetailsAuthenticationProvider抽象类,而AbstractUserDetailsAuthenticationProvider抽象类又实现了AuthenticationProvider这个接口。

AuthenticationProvider接口和AuthenticationManager接口都有 authenticate() 这个方法

认证流程:

1、传入用户名和密码

2、UsernamePasswordAuthenticationFilter会把用户名和密码封装成Authentication对象

3、然后又再调用AuthenticationManager接口中的authenticate()方法进行认证,在AuthenticationManager接口的实现类ProviderManager中又调用了重写的authenticate()方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

4、AbstractUserDetailsAuthenticationProviderauthenticate()方法中调用了抽象方法retrieveUser()方法

5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法

6、loadUserByUsername()方法会返回UserDetails对象,认证成功逐一返回上一层

2、前后端分离认证流程

前后端分离后,我们要求在认证成功或者失败的时候能够返回对应的状态码,这时我们不再使用Session进行认证管理,而常采用jwt(JSON Web Token)的方式进行认证,这里引出两种前后端分离的写法

SpringSecurity前后端分离_第2张图片

(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)

无论使用下面哪一种写法,这里都需要在UsernamePasswordAuthenticationFilter前面添加一个过滤器,用于进行Token认证,如果Token认证成功,则表示该用户已登录;Token认证失败则表明未登录或者登陆已过期。

2.1、继承UsernamePasswordAuthenticationFilter的写法

SpringSecurity前后端分离_第3张图片

认证流程:

1、传入用户名和密码

2、MyUsernamePasswordAuthenticationFilter会把用户名和密码封装成Authentication对象

3、然后又再调用AuthenticationManager接口中的authenticate()方法进行认证,在AuthenticationManager接口的实现类ProviderManager中又调用了重写的authenticate()方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

4、AbstractUserDetailsAuthenticationProviderauthenticate()方法中调用了抽象方法retrieveUser()方法

5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法

6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象

7、在继承WebSecurityConfigurerAdapter的类中设置登陆成功、失败处理器,处理器内部定义好返回的状态码等信息

2.2、自定义写法

SpringSecurity前后端分离_第4张图片

UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken抽象类,AbstractAuthenticationToken抽象类实现了Authentication接口

认证流程:

1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层

2、Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象

3、然后调用AuthenticationManagerauthenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

4、AbstractUserDetailsAuthenticationProviderauthenticate()方法中调用了抽象方法retrieveUser()方法

5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法

6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象

2.3、区别

1、使用UsernamePasswordAuthenticationFilter的写法需要使用登陆成功、失败处理器,自定义的写法不需要,自定义的写法可以自定义失败处理器(包括认证异常和授权异常,即登陆失败和没有权限)

2、使用UsernamePasswordAuthenticationFilter的写法对于扩展写法没那么友好,比如说添加手机验证码

二、数据库的设计

该示例是上面自定义的前后端分离的写法

这里使用的是Oracle数据库,这里没有权限的表,但是使用角色来判断也差不多

SpringSecurity前后端分离_第5张图片

1、用户表

SpringSecurity前后端分离_第6张图片

2、用户角色关系表

SpringSecurity前后端分离_第7张图片

3、角色表

SpringSecurity前后端分离_第8张图片

4、图片表

SpringSecurity前后端分离_第9张图片

5、点赞表

SpringSecurity前后端分离_第10张图片

三、初始配置

SpringBoot 版本是 2.6.0

1、项目结构

SpringSecurity前后端分离_第11张图片 SpringSecurity前后端分离_第12张图片

2、导入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>

    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <optional>trueoptional>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-securityartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.securitygroupId>
        <artifactId>spring-security-testartifactId>
        <scope>testscope>
    dependency>

    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
        <version>3.4.1version>
    dependency>

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>

    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>fastjsonartifactId>
        <version>1.2.74version>
    dependency>

    
    <dependency>
        <groupId>cn.hutoolgroupId>
        <artifactId>hutool-allartifactId>
        <version>5.5.6version>
    dependency>
    
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-generatorartifactId>
        <version>3.4.1version>
    dependency>
    
    <dependency>
        <groupId>org.apache.commonsgroupId>
        <artifactId>commons-lang3artifactId>
        <version>3.7version>
    dependency>
    
    <dependency>
        <groupId>org.apache.velocitygroupId>
        <artifactId>velocity-engine-coreartifactId>
        <version>2.2version>
    dependency>
    
    <dependency>
        <groupId>io.springfoxgroupId>
        <artifactId>springfox-swagger-uiartifactId>
        <version>2.7.0version>
    dependency>

    <dependency>
        <groupId>io.springfoxgroupId>
        <artifactId>springfox-swagger2artifactId>
        <version>2.7.0version>
    dependency>

    
    <dependency>
        <groupId>io.jsonwebtokengroupId>
        <artifactId>jjwtartifactId>
        <version>0.9.0version>
    dependency>

    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <scope>runtimescope>
    dependency>

    
    <dependency>
        <groupId>com.oracle.database.jdbcgroupId>
        <artifactId>ojdbc8artifactId>
        <scope>runtimescope>
    dependency>

dependencies>

3、代码生成器

代码生成器这里最开始使用的是mysql 8.X版本的,读者需要自己修改一下数据库的名字,如果是mysql 5.X还需要修改一下驱动

后面才改用Oracle数据库,这里的代码就懒得改了

package com.guet.APPshareimage;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.apache.commons.lang3.StringUtils;

import java.util.Scanner;

/**
 * @Author LZDWTL
 * @Date 2021-12-15 17:09
 * @ClassName CodeGenerator
 * @Description 代码生成器
 */
public class CodeGenerator {

    /**
     * 

* 读取控制台内容 *

*/
public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotEmpty(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 创建代码生成器对象 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir(scanner("请输入你的项目路径") + "/src/main/java"); //作者 gc.setAuthor("LZDWTL"); //生成之后是否打开资源管理器 gc.setOpen(false); //重新生成时是否覆盖文件 gc.setFileOverride(false); //%s 为占位符 //mp生成service层代码,默认接口名称第一个字母是有I gc.setServiceName("%sService"); //设置主键生成策略 自动增长 gc.setIdType(IdType.AUTO); //设置Date的类型 只使用 java.util.date 代替 gc.setDateType(DateType.ONLY_DATE); //开启实体属性 Swagger2 注解 gc.setSwagger2(true); mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/shareimage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("shareimage"); dsc.setPassword("888888"); //使用mysql数据库 dsc.setDbType(DbType.MYSQL); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); //pc.setModuleName(scanner("请输入模块名")); pc.setParent("com.guet.APPshareimage"); pc.setController("controller"); pc.setService("service"); pc.setServiceImpl("service.impl"); pc.setMapper("mapper"); pc.setEntity("entity"); pc.setXml("mapper"); mpg.setPackageInfo(pc); // 策略配置 StrategyConfig strategy = new StrategyConfig(); //设置哪些表需要自动生成 strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); //实体类名称驼峰命名 strategy.setNaming(NamingStrategy.underline_to_camel); //列名名称驼峰命名 strategy.setColumnNaming(NamingStrategy.underline_to_camel); //使用简化getter和setter strategy.setEntityLombokModel(true); //设置controller的api风格 使用RestController strategy.setRestControllerStyle(true); //驼峰转连字符 strategy.setControllerMappingHyphenStyle(true); //忽略表中生成实体类的前缀 //strategy.setTablePrefix("t_"); mpg.setStrategy(strategy); mpg.execute(); } }

运行代码生成器,复制路径输入,然后依次输入数据库中表的名字

D:\WorkSpace\JavaWorkSpce\ideal\APP-shareimage\APP-shareimage

t_user,t_picture,t_like,t_user_role,t_role

SpringSecurity前后端分离_第13张图片

4、配置application.yml

根据自己的数据库和redis进行配置

server:
  port: 8080

spring:
  # 数据库配置
  datasource:
    driver-class-name: oracle.jdbc.driver.OracleDriver
    url: jdbc:oracle:thin:@120.77.80.135:1521:orcl
    username: XXXXXX
    password: XXXXXX
    # 连接池
    hikari:
      # 连接池名
      pool-name: DateHikariCP
      # 最小空闲连接数
      minimum-idle: 5
      # 空闲连接最大存活时间,默认600000(10分钟)
      idle-timeout: 180000
      # 最大连接数,默认10
      maximum-pool-size: 10
      # 从连接池返回的连接自动提交
      auto-commit: true
      # 连接最大存活时间,1800000(30分钟)
      max-lifetime: 1800000
      # 连接超时时间,默认30000(30秒)
      connection-timeout: 30000
      # 测试连接是否可用的查询语句
      #connection-test-query: SELECT 1   #这个是mysql的测试语句
      connection-test-query: SELECT * from dual  #这个是oracle的测试语句

  #redis配置
  redis:
    #服务器地址
    host: 120.77.80.135
    #端口
    port: 6379
    #redis密码
    password: XXXXXX
    #数据库,默认是0
    database: 0
    #超时时间
    timeout: 1209600000ms
    lettuce:
      pool:
        #最大链接数,默认8
        max-active: 8
        #最大连接阻塞等待时间,默认-1
        max-wait: 10000ms
        #最大空闲连接,默认8
        max-idle: 200
        #最小空闲连接,默认0
        min-idle: 5

mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  type-aliases-package: com.guet.APPshareimage.entity

logging:
  level:
    com.guet.shareimage.mapper: debug

jwt:
  # JWT存储的请求头
  tokenHeader: Authorization
  # JWT 加解密使用的密钥
  secret: lzdwtl
  # JWT的超期限时间(1000*60*60*24*14)14天,即两周
  expiration: 1209600000
  # JWT 负载中拿到开头
  tokenHead: Bearer


role:
  roleid: 1

5、其他配置、工具类

5.1、SpringSecurity配置类

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

    @Autowired
    private MyOncePerRequestFilter myOncePerRequestFilter;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //1、关闭csrf,关闭Session
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //2、设置不需要认证的URL
        http
                .authorizeRequests()
                //允许未登录的用户进行访问
                .antMatchers("/doLogin").anonymous()
                //其余url都要认证才能访问
                .anyRequest().authenticated();
    }
}

5.2、JSON格式返回配置类

public abstract class JSONAuthentication {
    /**
     * 输出JSON
     *
     * @param request
     * @param response
     * @param obj
     * @throws IOException
     * @throws ServletException
     */
    protected void WriteJSON(HttpServletRequest request,
                             HttpServletResponse response,
                             Object obj) throws IOException, ServletException {
        //这里很重要,否则页面获取不到正常的JSON数据集
        response.setContentType("application/json;charset=UTF-8");

        //跨域设置
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Method", "POST,GET");
        //输出JSON
        PrintWriter out = response.getWriter();
        out.write(JSON.toJSONString(obj));
        out.flush();
        out.close();
    }
}

5.3、密码编码类

@Component
public class BCryptPasswordEncoderUtil extends BCryptPasswordEncoder {

    @Override
    public String encode(CharSequence rawPassword) {
        return super.encode(rawPassword);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {

        return super.matches(rawPassword,encodedPassword);
    }

}

5.4、JWT工具类

@Component
public class JwtUtil {

    private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);

    private static String SECRET_KEY;

    private static Long EXPIRATION_TIME;


    //对于静态变量,需要使用set方法才能使用设置好的字段值
    @Value("${jwt.secret}")
    public void setSECRET_KEY(String SECRET_KEY) {
        this.SECRET_KEY = SECRET_KEY;
    }

    @Value("${jwt.expiration}")
    public void setEXPIRATION_TIME(Long expiration) {
        this.EXPIRATION_TIME = expiration;
    }
    
    public static String getUUID() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jtw
     *
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     *
     * @param subject   token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = EXPIRATION_TIME;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("LZDWTL")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     *
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成加密后的秘钥 secretKey
     *
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(SECRET_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}

5.5、Redis工具类

@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}

5.6、Redis配置类

package com.guet.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @Author LZDWTL
 * @Date 2022-01-30 19:39
 * @ClassName
 * @Description
 */
@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

5.7、序列化工具

public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }


    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

四、全局异常处理

1、公用返回对象

1.1、拓展接口

使公用返回对象枚举类和自定义异常方便扩展

/**
 * @Author LZDWTL
 * @Date 2021-12-06 15:59
 * @ClassName CommonResp
 * @Description 返回对象的接口,装饰者模式
 */
public interface CommonResp {
    Integer getCode();

    String getMsg();

    CommonResp setMsg(String msg);
}

1.2、公用返回对象

@Data
public class RespBean implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer code;
    private String msg;
    private Object obj;

    public RespBean(RespBeanEnum respBeanEnum, Object obj) {
        this.code = respBeanEnum.getCode();
        this.msg = respBeanEnum.getMsg();
        this.obj = obj;
    }

    public RespBean(RespBeanEnum respBeanEnum) {
        this.code = respBeanEnum.getCode();
        this.msg = respBeanEnum.getMsg();
    }

    public RespBean(RespBeanEnum respBeanEnum, String msg) {
        this.code = respBeanEnum.getCode();
        this.msg = msg;
    }

    public RespBean() {
        this.code = RespBeanEnum.ERROR.getCode();
        this.msg = RespBeanEnum.ERROR.getMsg();
    }

    public RespBean(String msg) {
        this.code = RespBeanEnum.ERROR.getCode();
        this.msg = msg;
    }

    //自定义的业务异常错误码和信息
    public RespBean(ServicesException e) {
        this.code = e.getCode();
        this.msg = e.getMsg();
    }

}

1.3、枚举类

public enum RespBeanEnum implements CommonResp{

    SUCCESS(200,"请求成功!"),
    ERROR(500,"服务器响应错误!"),

    /** 10XX 表示用户错误*/
    USER_REGISTER_FAILED(1001, "注册失败"),
    USER_ACCOUNT_EXISTED(1002,"用户名已存在"),
    USER_ACCOUNT_NOT_EXIST(1003,"用户名不存在"),
    USERNAME_PASSWORD_ERROR(1004,"用户名或密码错误"),
    PASSWORD_ERROR(1005,"密码错误"),
    USER_ACCOUNT_EXPIRED(1006,"账号过期"),
    USER_PASSWORD_EXPIRED(1007,"密码过期"),
    USER_ACCOUNT_DISABLE(1008,"账号不可用"),
    USER_ACCOUNT_LOCKED(1009,"账号锁定"),
    USER_NOT_LOGIN(1010,"用户未登陆"),
    USER_NO_PERMISSIONS(1011,"用户权限不足"),
    USER_SESSION_INVALID(1012,"会话已超时"),
    USER_ACCOUNT_LOGIN_IN_OTHER_PLACE(1013,"账号超时或账号在另一个地方登陆"),
    TOKEN_VALIDATE_FAILED(1014,"Token令牌验证失败"),
    LIKE_ALREADY_GICED(1015,"请勿重复点赞"),



    /** 20XX 表示服务器错误 */
    PICTURE_UPLOAD_FAILED(2001,"上传图片失败"),
    GIVE_LIKE_FAILED(2002,"点赞失败"),
    PICTURE_LOAD_FAILED(2003,"图片加载失败"),
    UPDATE_USER_INFO_FAILED(2004,"修改用户信息失败"),
    UPDATE_USER_PASSWORD_FAILED(2005,"修改密码失败"),
    ;

    private Integer code;
    private String msg;

    RespBeanEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    @Override
    public Integer getCode() {
        return this.code;
    }

    @Override
    public String getMsg() {
        return this.msg;
    }

    @Override
    public CommonResp setMsg(String msg) {
        this.msg=msg;
        return this;
    }
}

2、全局异常

2.1、自定义异常

实现CommonResp接口,方便自定义异常后续修改错误信息

public class ServicesException extends RuntimeException implements CommonResp {

    private CommonResp commonResp;

    //直接接收RespBeanEnum的传参用于构造业务异常
    public ServicesException(CommonResp commonResp) {
        super();    //调用父类的无参构造方法
        this.commonResp = commonResp;
    }

    //接收自定义msg的方式构造业务异常
    public ServicesException(String msg, CommonResp commonResp) {
        super();
        this.commonResp = commonResp;
        this.commonResp.setMsg(msg);
    }


    @Override
    public Integer getCode() {
        return this.commonResp.getCode();
    }

    @Override
    public String getMsg() {
        return this.commonResp.getMsg();
    }

    @Override
    public CommonResp setMsg(String msg) {
        this.commonResp.setMsg(msg);
        return this;
    }
}

2.2、全局异常处理器

@RestControllerAdvice注解表示捕获控制层抛出的异常

@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常

SpringSecurity前后端分离_第14张图片

(图片来源:https://blog.csdn.net/weixin_43702146/article/details/118606502)

因为使用了@RestControllerAdvice注解,自动去捕获控制层抛出的异常,AuthenticationException异常和AccessDeniedException异常也被捕获了,但是我不想在这里处理,所以将这两个异常往外抛给失败处理器去处理。

@RestControllerAdvice  //捕获controller层的异常
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * @Author: LZDWTL
     * @param: [e]
     * @return: com.guet.shareimage.response.RespBean
     * @Description: 业务异常
     */
    @ExceptionHandler(value = ServicesException.class)
    public RespBean servicesExceptionHandler(ServicesException e){
        logger.error("发生业务异常! 原因是:{}",e.getMsg());
        return new RespBean(e);
    }


    /**
     * @Author: LZDWTL
     * @param: [e]
     * @return: com.guet.shareimage.response.RespBean
     * @Description: 其他异常
     */
    @ExceptionHandler(value = Exception.class)
    public RespBean exceptionHandler(Exception e){
        logger.error("未知异常! 原因是:",e);
        return new RespBean();
    }


    /**
     * @Author: LZDWTL
     * @Date: 2022/2/11
     * @param: [authException]
     * @return: void
     * @Description: 将 AuthenticationException 异常往上抛,让认证处理器去处理
     */
    @ExceptionHandler(value = AuthenticationException.class)
    public void accountExpiredExceptionHandler(AuthenticationException authException){
        throw authException;
    }

    //将 AccessDeniedException 异常往上抛,让授权处理器去处理
    @ExceptionHandler(value = AccessDeniedException.class)
    public void accessDeniedExceptionHandler(AccessDeniedException accDenException){
        throw accDenException;
    }
}

五、登陆认证

SpringSecurity前后端分离_第15张图片

UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken抽象类,AbstractAuthenticationToken抽象类实现了Authentication接口

认证流程:

1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层

2、Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象

3、然后调用AuthenticationManagerauthenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

4、AbstractUserDetailsAuthenticationProviderauthenticate()方法中调用了抽象方法retrieveUser()方法

5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法

6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象

1、登陆模块

包括登陆和登出功能

1.1、控制器LoginController

/**
 * @Author LZDWTL
 * @Date 2021-12-17 8:57
 * @ClassName LoginController
 * @Description 登陆控制器
 * 这个控制器没有用到,“/login”这个url是SpringSecurity中的UsernamePasswordAuthenticationFilter拦截器中自己设定的
 * 同时它还设置了必须使用POST方式才能进行登陆
 */
@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/doLogin")
    public RespBean doLogin(@RequestBody LoginDTO loginDTO){
        return loginService.doLogin(loginDTO);
    }

    @RequestMapping("/doLogout")
    public RespBean doLogout(){
        return loginService.doLogout();
    }
}

1.2、业务层

Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象

1.2.1、LoginService
public interface LoginService {
    RespBean doLogin(LoginDTO loginDTO);
    RespBean doLogout();
}
1.2.2、LoginServiceImpl

这里的 AuthenticationManager 需要在 SpringSecurity 中使用 authenticationManagerBean() 方法才能调用AuthenticationManagerauthenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

这里把生成的Token和查询到的用户信息存到Redis中,方便后续使用

@Service
public class LoginServiceImpl implements LoginService {

    private static final Logger logger = LoggerFactory.getLogger(LoginServiceImpl.class);

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

    @Autowired
    private TUserService userService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private BCryptPasswordEncoderUtil passwordEncoder;

    @Autowired
    private RedisCache redisCache;


    /**
     * @Author: LZDWTL
     * @param: [username, password]
     * @return: com.guet.APPshareimage.response.RespBean
     * @Description: 登陆
     */
    @Override
    public RespBean doLogin(LoginDTO loginDTO) {

        /**
         * 因为我使用了全局异常处理,GobalExceptionHandler会自动捕获controller层抛出的异常
         * authenticationManager.authenticate 这一句认证失败会抛出AuthenticationException异常
         * 我定义了认证失败处理器无法获取到 AuthenticationException 异常,因为全局异常处理已经捕获了
         * 然后 AuthenticationException 异常不属于 ServicesException,所以会返回500,服务器响应错误
         */
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        if (Objects.isNull(authenticate)) {
            //用户名密码错误
            throw new ServicesException(RespBeanEnum.USERNAME_PASSWORD_ERROR);
        }
        AuthUser authUser = (AuthUser) authenticate.getPrincipal();
        String username = authUser.getTUser().getUsername();
        String token = JwtUtil.createJWT(username);

        //把token和用户信息存到redis中
        redisCache.setCacheObject("Token_" + username, token);
        redisCache.setCacheObject("UserDetails_" + username, authUser);

        //将用户存入上下文中
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        Map<String, String> map = new HashMap<>();
        map.put("token", token);
        return new RespBean(RespBeanEnum.SUCCESS, map);
    }

    @Override
    public RespBean doLogout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        AuthUser authUser = (AuthUser) authentication.getPrincipal();
        String username = authUser.getTUser().getUsername();

        //删除redis中存的信息
        redisCache.deleteObject("Token_" + username);
        redisCache.deleteObject("UserDetails_" + username);
        //清除上下文
        SecurityContextHolder.clearContext();

        return new RespBean(RespBeanEnum.SUCCESS);
    }
}

SpringSecurity前后端分离_第16张图片

1.2.3、TUserService
public interface TUserService extends IService<TUser> {

    TUser getUserByUserName(String username);

}
1.2.4、TUserServiceImpl

TUserMapper需要继承BaseMapper才能使用selectOne()这个方法

@Service
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements TUserService {

    @Value("${role.roleid}")
    private Integer roleId;

    @Autowired
    private TUserMapper userMapper;


    /**
     * @Author: LZDWTL
     * @Date: 2021/12/28
     * @param: [username]
     * @return: com.guet.response.RespBean
     * @Description: 通过用户名获取用户
     */
    @Override
    public TUser getUserByUserName(String username) {
        LambdaQueryWrapper<TUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        //查询条件:全匹配账号名,和状态为1的账号
        lambdaQueryWrapper
                .eq(TUser::getUsername, username);

        //用getOne查询一个对象出来
//        TUser user = this.getOne(lambdaQueryWrapper);

        TUser user = userMapper.selectOne(lambdaQueryWrapper);   //这个与上面的getOne有无区别?

        return user;
    }
}

1.3、实现 UserDetails 接口

/**
 * @Author LZDWTL
 * @Date 2021-12-15 23:35
 * @ClassName AuthUser
 * @Description 实现UserDetails,仿写User的原因是 防止User类名和自己创建的实体类 User 重合(虽然我这里创建的不是User而是TUser)
 */
@Data
@AllArgsConstructor  //全参构造
@NoArgsConstructor  //无参构造
public class AuthUser implements UserDetails {

    private TUser tUser;

//    @JSONField(serialize = false)
    private Collection<? extends GrantedAuthority> authorities;

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

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

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

    // 账户是否未过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 账户是否未被锁
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

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

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

1.4、实现UserDetailsService 接口

重写UserDetailsService接口的 loadUserByUsername()方法,在loadUserByUsername()方法中,会查询用户和权限(这里没有权限表,所以查的是角色),然后返回UserDetails对象

/**
 * 要实现UserDetailsService接口,这个接口是security提供的
 */
@Service(value = "userDetailsService")
public class AuthUserDetailsServiceImpl implements UserDetailsService {

    private static final Logger logger = LoggerFactory.getLogger(AuthUserDetailsServiceImpl.class);

    @Autowired
    private TUserService userService;

    @Autowired
    private TRoleService roleService;

    /**
     * 通过账号查找用户、角色的信息
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        TUser user = userService.getUserByUserName(username);
        if (user == null) {
            //用户名不存在
            throw new ServicesException(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);
        } else {
            //查找角色,实际应该查询权限,但我数据库没有设计所以就查角色就好了
            List<String> roles = roleService.getRolesByUserName(username);
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            for (String role : roles) {
                authorities.add(new SimpleGrantedAuthority(role));
            }
            System.out.println("AuthUserDetailsServiceImpl-loadUserByUsername......user ===> " + user);
            return new AuthUser(user, authorities);
        }
    }
}

1.5、Mapper

1.5.1、TUserMapper
@Mapper
public interface TUserMapper extends BaseMapper<TUser> {

}

2、Token 认证模块

2.1、认证过滤器

/**
 * @Author LZDWTL
 * @Date 2021-12-20 16:28
 * @ClassName ${MyOncePerRequestFilter}
 * @Description ${认证过滤器}
 */
@Component
public class MyOncePerRequestFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(MyOncePerRequestFilter.class);

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

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        // header的值是在yml文件中定义的 “Authorization”
        String token = request.getHeader(header);
        System.out.println("MyOncePerRequestFilter-token = " + token);
        if (!StrUtil.isEmpty(token)) {
            String username = null;
            try {
                Claims claims = JwtUtil.parseJWT(token);
                username = claims.getSubject();
            } catch (Exception e) {
                e.printStackTrace();
//                throw new ServicesException("非法Token,请重新登陆", RespBeanEnum.ERROR);
                WriteJSON(request,response,new RespBean(RespBeanEnum.ERROR,"非法Token,请重新登陆"));
                return;
            }
            String redisToken = redisCache.getCacheObject("Token_" + username);
            System.out.println("MyOncePerRequestFilter-redisToken = " + redisToken);
            if (StrUtil.isEmpty(redisToken)) {
                //token令牌验证失败
//                throw new ServicesException(RespBeanEnum.TOKEN_VALIDATE_FAILED);

                //输出JSON
                WriteJSON(request,response,new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED));
                return;
            }

            //对比前端发送请求携带的的token是否与redis中存储的一致
            if (!Objects.isNull(redisToken) && redisToken.equals(token)) {
                AuthUser authUser = redisCache.getCacheObject("UserDetails_" + username);
                System.out.println("MyOncePerRequestFilter-authUser = " + authUser);
                if (Objects.isNull(authUser)) {
//                    throw new ServicesException(RespBeanEnum.USER_NOT_LOGIN);
                    WriteJSON(request,response,new RespBean(RespBeanEnum.USER_NOT_LOGIN));
                    return;
                }
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
    private void WriteJSON(HttpServletRequest request,
                             HttpServletResponse response,
                             Object obj) throws IOException, ServletException {
        //这里很重要,否则页面获取不到正常的JSON数据集
        response.setContentType("application/json;charset=UTF-8");

        //跨域设置
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Method", "POST,GET");
        //输出JSON
        PrintWriter out = response.getWriter();
        out.write(JSON.toJSONString(obj));
        out.flush();
        out.close();
    }
}


2.2、SpringSecuity配置类

在配置类中使用addFilterBefore()方法让认证过滤器MyOncePerRequestFilter添加在UsernamePasswordAuthenticationFilter这个过滤器前面

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

    @Autowired
    private MyOncePerRequestFilter myOncePerRequestFilter;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //1、关闭csrf,关闭Session
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //2、设置不需要认证的URL
        http
                .authorizeRequests()
                //允许未登录的用户进行访问
                .antMatchers("/doLogin").anonymous()
                //其余url都要认证才能访问
                .anyRequest().authenticated();

        //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
        http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);

    }
}

SpringSecurity前后端分离_第17张图片

六、鉴权

下面列举了两种鉴权方式,分别是注解鉴权和动态鉴权

1、注解鉴权

使用 @PreAuthorize注解需要在SpringSecurity配置类中使用下面的语句才能开启方法级的安全

@EnableGlobalMethodSecurity(prePostEnabled = true)
@RestController
@RequestMapping("/user")
public class TUserController {

    @RequestMapping("/hello")
    //对于hasRole这个方法来讲,ROLE_ 加不加都可以,它的方法会自动判断的
    @PreAuthorize("hasRole('ROLE_user')")
    public String test() {
        return "Hello Login Success!";
    }

}

这样就可以了,因为前面已经写好了一些关联的代码,所以在访问该URL的时候,会执行hasRole()这个方法,然后查询AuthUser类(AuthUser类就是实现了UserDetails接口的实现类)中的属性authorities,只要authorities中包含"ROLE_user",则该用户就可以访问这个URL,否则会报错,提示权限不足。

注意访问一些需要认证后才能访问的URL时,记得带上token和content-type。

我这里的token的key是Authorization,这个是在application.yml文件中定义的,可以自行修改

SpringSecurity前后端分离_第18张图片

2、动态鉴权

这里写的动态鉴权需要数据库中新创建两个表,分别是菜单表t_menu和角色菜单关系表t_role_menu,菜单表中存放前端需要访问的url地址
下面编写鉴权类

@Component("rbacService")
public class MyRBACService {
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        if (principal instanceof UserDetails) {
            UserDetails userDetails=(UserDetails)principal;

            /**
             * 该方法主要对比认证过的用户是否具有请求URL的权限,有则返回true
             */
            //本次要访问的资源
            SimpleGrantedAuthority simpleGrantedAuthority=new SimpleGrantedAuthority(request.getRequestURI());

            //用户拥有的权限中是否包含请求的url
            return userDetails.getAuthorities().contains(simpleGrantedAuthority);
        }

        return false;
    }
}

在SpringSecurity配置类中设置鉴权规则

@Override
    protected void configure(HttpSecurity http) throws Exception {

        //1、关闭csrf,关闭Session
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //2、设置不需要认证的URL
        http
                .authorizeRequests()
                //允许未登录的用户进行访问
                .antMatchers("/user/doLogin").permitAll()
//                .antMatchers("/swagger-ui.html","/user/test").permitAll()
                //其余url都要认证才能访问
//                .anyRequest().authenticated()
                //鉴权规则
                .anyRequest().access("@rbacService.hasPermission(request,authentication)");

        //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
        http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);


        //4、异常处理
        http
                .exceptionHandling()
                //认证失败处理器
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                //权限不足处理器
                .accessDeniedHandler(myAccessDeniedHandler);

        //5、允许跨域
        http.cors();
    }

七、自定义失败处理器

1、认证失败处理器

继承自定义的JSON格式输出类JSONAuthentication输出JSON格式,同时在里面判断是什么异常做针对性输出

@Component
public class MyAuthenticationEntryPoint extends JSONAuthentication implements AuthenticationEntryPoint {

    private static final Logger logger = LoggerFactory.getLogger(MyAuthenticationEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        //用户未登录或者身份校验失败
//        RespBean respBean = new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED);
//        this.WriteJSON(request, response, respBean);

        RespBean respBean;
        if (authException instanceof AccountExpiredException) {
            //账号过期
            respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_EXPIRED);
        } else if (authException instanceof InternalAuthenticationServiceException) {
            //用户不存在
            respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);
        } else if (authException instanceof BadCredentialsException) {
            //用户名或密码错误(也就是用户名匹配不上密码)
            respBean = new RespBean(RespBeanEnum.USERNAME_PASSWORD_ERROR);
        } else if (authException instanceof CredentialsExpiredException) {
            //密码过期
            respBean = new RespBean(RespBeanEnum.USER_PASSWORD_EXPIRED);
        } else if (authException instanceof DisabledException) {
            //账号不可用
            respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_DISABLE);
        } else if (authException instanceof LockedException) {
            //账号锁定
            respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_LOCKED);
        } else {
            //其他错误
            respBean = new RespBean(RespBeanEnum.USER_NOT_LOGIN);
        }

        //打印错误
        logger.error(String.valueOf(authException));

        //输出
        this.WriteJSON(request, response, respBean);
    }
}

2、权限不足处理器

@Component
public class MyAccessDeniedHandler extends JSONAuthentication implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

        //用户权限不足
        RespBean respBean = new RespBean(RespBeanEnum.USER_NO_PERMISSIONS);
        //输出
        this.WriteJSON(request, response, respBean);

    }
}

3、SpringSecurity配置

configure方法中配置失败处理器

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

    @Autowired
    private MyOncePerRequestFilter myOncePerRequestFilter;

    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //1、关闭csrf,关闭Session
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //2、设置不需要认证的URL
        http
                .authorizeRequests()
                //允许未登录的用户进行访问
                .antMatchers("/doLogin").anonymous()
                //其余url都要认证才能访问
                .anyRequest().authenticated();

        //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
        http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);


        //4、异常处理
        http
                .exceptionHandling()
                //认证失败处理器
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                //权限不足处理器
                .accessDeniedHandler(myAccessDeniedHandler);

    }
}

八、跨域

1、编写配置类

/**
 * 解决跨域问题
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                //允许任何域名
                .allowedOriginPatterns("*")
                //允许任何方法
                .allowedMethods("PUT", "DELETE", "GET", "POST", "OPTIONS")
                //允许任何头
                .allowedHeaders("*")
                //暴露头
                .exposedHeaders("access-control-allow-headers",
                        "access-control-allow-methods",
                        "access-control-allow-origin",
                        "access-control-max-age",
                        "X-Frame-Options")
                // 是否允许证书(cookies)
                .allowCredentials(true)
                .maxAge(3600);
    }
}

2、在SpringSecurity配置类中配置

在配置类的configure()方法中开启允许跨域

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

    @Autowired
    private MyOncePerRequestFilter myOncePerRequestFilter;

    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //1、关闭csrf,关闭Session
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //2、设置不需要认证的URL
        http
                .authorizeRequests()
                //允许未登录的用户进行访问
                .antMatchers("/doLogin").anonymous()
                //其余url都要认证才能访问
                .anyRequest().authenticated();

        //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
        http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);


        //4、异常处理
        http
                .exceptionHandling()
                //认证失败处理器
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                //权限不足处理器
                .accessDeniedHandler(myAccessDeniedHandler);

        //5、允许跨域
        http.cors();
    }
}

你可能感兴趣的:(SpringSecurity,java,spring,安全架构,web安全,装饰器模式)