1. 配置日志相关
    1.1 日志级别 Level

1.2 配置示例
1.3 配置示例详解

  1. 集成 Swagger,生成接口文档
    2.1 添加依赖

2.2 application.yml 中添加开关
2.3 编写 Swagger 配置类

  1. 配置文件敏感信息加密
    3.1 添加依赖

3.2 application.yml 配置
3.3 Jasypt 使用测试

  1. 数据源配置
    4.1 添加依赖

4.2 application.yml 配置
4.3 多数据源 DataSource 配置
4.4 多数据源使用示例

  1. 集成 Redis
    5.1 添加依赖

5.2 application.yml 配置
5.3 Redis 工具类
5.4 乱码处理

  1. 集成 Elasticsearch
    6.1 添加依赖

6.2 application.yml 配置
6.3 ElasticSearch 配置
6.4 示例

  1. 集成 Spring Security+JWT
    7.1 添加依赖

7.2 创建用户表实体类(User.java)和 jpa 接口(UserRepository.java)
7.2 用户 VO(UserVO.java)
7.3 登录成功后返回前端的用户信息(LoginSuccessVO.java)
7.4 自定义 UserDetailsService
7.5 自定义 AuthenticationSuccessHandler
7.6 自定义 AuthenticationFailureHandler
7.7 自定义 LogoutSuccessHandler
7.8 自定义 AuthenticationEntryPoint
7.9 自定义 AccessDeniedHandler
7.10 JWT 拦截
7.11 定义权限验证 RabcAuthorityService
7.12 配置 WebSecurityConfig

  1. 跨域设置
    本场 Chat 分享主要介绍 Spring Boot 开发过程使用到的一些组件,帮助开发人员快速搭建基础开发框架。
  2. 配置日志相关
    1.1 日志级别 Level

日志的行为等级分为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL

Log4j 建议只使用四个级别,优先级从高到低依次是:ERROR、WARN、INFO、DEBUG。

1.2 配置示例

  1. 配置 application.yml,指定 logback.xml 的详细地址。

logging:
config: classpath:logback-config.xml

  1. 配置 logback-config.xml,在配置文件中指定日志的打印级别,日志文件的存储路径等日志相关信息。
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds"
    debug="false">
    <property name="log.path" value="/project/logs/" />
    <!--输出到控制台 -->
    <appender name="console"
        class="ch.qos.logback.core.ConsoleAppender">
        <!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> 
            </filter> -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--输出到文件 -->
    <appender name="file"
        class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}${log.file}</file>
<!--         <rollingPolicy -->
<!--             class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> -->
<!--             <fileNamePattern>${log.path}deepev.%d{yyyy-MM-dd_HH-mm}.log -->
<!--             </fileNamePattern> -->
<!--             <maxHistory>30</maxHistory> -->
<!--             <totalSizeCap>1GB</totalSizeCap> -->
<!--         </rollingPolicy> -->

        <!-- 滚动策略 日期+大小 策略 -->
        <rollingPolicy
            class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${log.path}deepev.%d{yyyy-MM-dd}.log-%i.zip</fileNamePattern>
            <!-- 单个日志大小 -->
            <maxFileSize>30MB</maxFileSize>
            <!-- 日志保存周期 -->
            <maxHistory>30</maxHistory>
            <!-- 总大小 -->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="warn">
        <appender-ref ref="console" />
        <appender-ref ref="file" />
    </root>
    <!-- 指定业务包-->
    <logger name="com.**" level="INFO" additivity="false">
        <appender-ref ref="console" />
        <appender-ref ref="file" />
    </logger>

</configuration>

1.3 配置示例详解
根据上边的 logback-config.xml 文件,对 Logback 的相关配置进行详细讲解。

  1. 根节点<configuration> 有三个属性,scan、scanPeriod 和 debug。

scan 值为 true 或 false,用于监控配置文件,如果文件发生改变后,将会被重新加载,此属性默认为 true。
scanPeriod 是时间间隔,是用来设置监测配置文件是否有修改,默认的时间单位是毫秒,只有 scan 设置为 true 时,此属性才会生效。
debug 此属性值为 true 或者 false,用于控制是否打印出 Logback 内部日志信息,设置为 true 时,可以通过 Logback 的内部日志,实时查看 Logback 运行状态。默认值为 false。

  1. <appender> 节点是负责写日志的组件,有两个必须的属性,分别是 name 和 class,name 用于指定 appender 组件的名称,class 是 appender 组件的策略。

这里介绍一下 appender 的常用的组件:

ConsoleAppender 控制台组件,该组件是把日志输出到控制台上。上边示例中的 name=“console” 的组件就是这种策略。console 组件中的 <encoder> 部分是用来格式化日志格式的。
FileAppender 文件组件,该组件是把日志输出到文档中去。
<file>info.log</file>:指定日志被写入的文件的地址,该地址可以是相对目录,也可以是绝对目录。指定的目录如果不存在的话,会自动创建。
<append>true</append>:文件的写入类型,true 的话新生成的日志会追加到文件的结尾,如果为 false,则清空现有的文件。默认值为 true。
RollingFileAppender 滚动记录文件组件,该组件与 FileAppender 相比,增加了 rollingPolicy 属性,是对日志滚动处理时,指定对文件复制和重命名时的滚动的策略。
常用策略有三种:

ch.qos.logback.core.rolling.TimeBasedRollingPolicy:根据时间来制定滚动策略。
ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy:查看当前日志文件的大小,当超过指定大小时,会告知 RollingFileAppender,触发当前日志文件滚动。
ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy: 根据日期和大小同时确定滚动策略。

  1. <logger> 节点:用来设置某一个包或具体的某一个类的日志打印级别、以及指定<appender>。在 logger 节点下可以包含零个或多个<appender-ref> 元素,ref 值用于标识 appender 的 name,并添加到这个 logger 中。上面的示例中,添加了两个 appender-ref,分别指定 file。
  2. <root>节点,它是根 logger,是所有 logger 的上级,只有一个 level 的属性,用于指定日志等级。
  3. 集成 Swagger,生成接口文档
    Swagger 2

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。

2.1 添加依赖
这里我添加了两个 UI,springfox-swagger-ui 是官方提供的,swagger-bootstrap-ui 是第三方封装的,我平时更习惯使用第三方 UI。

<dependency>

<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>

</dependency>
<dependency>

<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.0</version>

</dependency>
<dependency>

<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.1</version>

</dependency>
2.2 application.yml 中添加开关
Swagger 只能在开发环境使用,部署生产环境时要关闭,为了方便管理,在配置文件中添加开关。

swagger:
enable: true
2.3 编写 Swagger 配置类
在该配置类中,可以配置 API 文档中的显示信息,通过全局配置,可以为所有的 API 接口添加通用条件。

下面的示例中为所有的接口添加 token 公共配置的方法。

package com...conf;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

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

@Configuration
@EnableSwagger2
public class SwaggerConfig {

@Value("${swagger.enable}")
private boolean enableSwagger;

@Bean
public Docket docket(){
    List<Parameter> params = getParam();
    return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).enable(enableSwagger).select()
            //当前包路径
            .apis(RequestHandlerSelectors.basePackage("com.reach.usercenter"))
            .paths(PathSelectors.any())
            .build()

// .securitySchemes(security()).securityContexts(securityContexts()); //添加全局 token

            .globalOperationParameters(params);//在每一个接口上添加 token
}

private List<Parameter> getParam(){
    ParameterBuilder ticketPar = new ParameterBuilder();
    List<Parameter> pars = new ArrayList<Parameter>();
    ticketPar.name("Authorization").description("user ticket")//Token 以及 Authorization 为自定义的参数,session 保存的名字是哪个就可以写成那个
            .modelRef(new ModelRef("string")).parameterType("header")
            .required(true).build(); //header 中的 ticket 参数非必填,传空也可以
    pars.add(ticketPar.build());    //根据每个方法名也知道当前方法在设置什么参数
    return pars;
}

private List<SecurityContext> securityContexts() {
    List<SecurityContext> securityContexts=new ArrayList<>();
    securityContexts.add(
            SecurityContext.builder()
                    .securityReferences(defaultAuth())
                    .forPaths(PathSelectors.regex("^(?!auth).*$"))
                    .build());
    return securityContexts;
}
private List<SecurityReference> defaultAuth() {
    AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
    AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
    authorizationScopes[0] = authorizationScope;
    List<SecurityReference> securityReferences=new ArrayList<>();
    securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
    return securityReferences;
}

private List<ApiKey> security() {
    ApiKey key = new ApiKey("Authorization", "Authorization ", "header");
    List<ApiKey> list = new ArrayList();
    list.add(key);
    return list;
}

//构建 api 文档的详细信息函数
private ApiInfo apiInfo(){
    return new ApiInfoBuilder()
            .title("EV DATA RESTful API")
            .version("1.0")
            .description("API 描述")
            .build();
}

}

  1. 配置文件敏感信息加密
    在开发的服务中,为了安全考虑,有一些信息是必须要进行加密处理的,比如数据库的密码等信息,开发环境和测试环境还好说,但是生产环境必须要保证信息安全。Jasypt 是一个通用的加密库,我们可以使用 Jasypt 对配置文件中的敏感信息进行加密处理。

3.1 添加依赖
<!-- https://mvnrepository.com/artifact/com.github.ulisesbocchio/jasypt-spring-boot-starter -->
<dependency>

<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.2</version>

</dependency>
3.2 application.yml 配置

配置文件项加解密密码,此处是开发环境,实际生产中应该注销,使用启动参数的方式传入

jasypt:
encryptor:

password: testtest

spring:
datasource:

url: jdbc:......
username: root
password: ENC(0RVCs2UAXrb8cWsMk8tJuE6XXMuKZxAHRJ1yP9dA4jdY1MU1jKJumLyVUiF0yvuD)

......

3.3 Jasypt 使用测试
通过上边的配置以后,我们就完成了对数据库密码的加密工作,Spring Boot 已经可以正常启动了。更为详细的配置可以参考一下网站:

https://github.com/ulisesbocchio/jasypt-spring-boot
http://www.jasypt.org/easy-usage.html
下边是 Jasypt 的测试例子,实现对信息的加密和解密处理。

package com.**;

import org.jasypt.encryption.StringEncryptor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class UserEncryptorTest {

@Autowired
private StringEncryptor stringEncryptor;

//加密
@Test
public void encrypt() {
    System.out.println("testtest 的密文为:"+    stringEncryptor.encrypt("testtest"));
}

//解密
@Test
public void decrypt() {
    System.out.println("解密结果为:"+stringEncryptor.decrypt("+VZcKfSsRgHuXnosbV55R8elvOHMEoThfgKX+ujJDp0pzMeZLZLK3yrcFWPn2iiO"));
}

}

//输出结果
//testtest 的密文为:+VZcKfSsRgHuXnosbV55R8elvOHMEoThfgKX+ujJDp0pzMeZLZLK3yrcFWPn2iiO
//解密结果为:testtest

  1. 数据源配置
    这里使用 MySQL 数据库,使用 JDBC 和 JPA 操作数据库。

4.1 添加依赖
<dependency>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>
<dependency>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>
<dependency>

<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>

</dependency>
4.2 application.yml 配置
配置文件中包含了单数据源和多数据源的配置。

单数据源配置

spring:
datasource:

url: jdbc:mysql://......
username: root
password: ENC(0RVCs2UAXrb8cWsMk8tJuE6XXMuKZxAHRJ1yP9dA4jdY1MU1jKJumLyVUiF0yvuD)
hikari:
  max-lifetime: 30000
  max-active: 20
  max-idle: 8
  min-idle: 0
  initial-size: 10
  driver-class-name: com.mysql.cj.jdbc.Driver

多数据源配置

spring:
datasource:

primary:
  jdbc-url: jdbc:mysql://......
  username: root
  password: ENC(0RVCs2UAXrb8cWsMk8tJuE6XXMuKZxAHRJ1yP9dA4jdY1MU1jKJumLyVUiF0yvuD)
  hikari:
    max-lifetime: 30000
    max-active: 20
    max-idle: 8
    min-idle: 0
    initial-size: 10
secondary:
  jdbc-url: jdbc:mysql://......
  username: root
  password: ENC(0RVCs2UAXrb8cWsMk8tJuE6XXMuKZxAHRJ1yP9dA4jdY1MU1jKJumLyVUiF0yvuD)
  hikari:
    max-lifetime: 30000
    max-active: 20
    max-idle: 8
    min-idle: 0
    initial-size: 10
driver-class-name: com.mysql.cj.jdbc.Driver

jpa 配置

jpa:

hibernate:
  ddl-auto: update
properties:
  hibernate:
    dialect: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true

4.3 多数据源 DataSource 配置
如果仅使用单数据源的话,第一步和第二步完成后就可以正常使用了,但是如果是多数据源的话,需要对 DataSource 进行配置后再使用。

JDBC 配置

package com.**.conf;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

@Bean(name = "priDataSource")
@Qualifier("priDataSource")
@Primary
@ConfigurationProperties(prefix="spring.datasource.primary")
public DataSource dvDataSource() {
    return DataSourceBuilder.create().build();
}

@Bean(name = "secDataSource")
@Qualifier("secDataSource")
@ConfigurationProperties(prefix="spring.datasource.secondary")
public DataSource bcmDataSource() {
    return DataSourceBuilder.create().build();
}

@Bean(name = "jdbcTemplate")
public JdbcTemplate dvJdbcTemplate(
        @Qualifier("priDataSource") DataSource dataSource) {
    return new JdbcTemplate(dataSource);
}

@Bean(name = "bcmJdbcTemplate")
public JdbcTemplate bcmJdbcTemplate(
        @Qualifier("secDataSource") DataSource dataSource) {
    return new JdbcTemplate(dataSource);
}

}
JPA 配置

PrimaryJPAConfig.java

package com.**.conf;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManager;
import javax.sql.DataSource;
import java.util.Map;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(

    entityManagerFactoryRef="entityManagerFactoryDv",
    transactionManagerRef="transactionManagerDv",
    basePackages = {"com.**.pri"})

public class PrimaryJPAConfig {

@Autowired
private HibernateProperties hibernateProperties;

@Autowired
private JpaProperties jpaProperties;

@Autowired
@Qualifier("priDataSource")
private DataSource priDataSource;

@Primary
@Bean(name="entityManagerPri")
public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
    return entityManagerFactoryPri(builder).getObject().createEntityManager();
}

@Primary
@Bean(name="entityManagerFactoryPri")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryPri(EntityManagerFactoryBuilder builder) {
    Map<String,Object> properties = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(),new HibernateSettings());
    return builder.dataSource(priDataSource)
            .properties(properties)
            .packages("com.**.pri") //次数据源实体所在位置
            .persistenceUnit("dvPersistenceUnit")
            .build();
}

@Primary
@Bean(name="transactionManagerDv")
public PlatformTransactionManager transactionManagerDv(EntityManagerFactoryBuilder builder) {
    return new JpaTransactionManager(entityManagerFactoryPri(builder).getObject());
}

}
SecJPAConfig.java

package com.**.conf;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManager;
import javax.sql.DataSource;
import java.util.Map;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(

    entityManagerFactoryRef="entityManagerFactorySec",
    transactionManagerRef="transactionManagerSec",
    basePackages= {"com.**.sec"})

public class SecJPAConfig {

@Autowired
private HibernateProperties hibernateProperties;

@Autowired
private JpaProperties jpaProperties;

@Autowired
@Qualifier("secDataSource")
private DataSource secondaryDataSource;

@Bean(name="entityManagerSec")
public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
    return entityManagerFactorySec(builder).getObject().createEntityManager();
}

@Bean(name="entityManagerFactorySec")
public LocalContainerEntityManagerFactoryBean entityManagerFactorySec(EntityManagerFactoryBuilder builder) {
    Map<String,Object> properties = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(),new HibernateSettings());
    return builder.dataSource(secondaryDataSource)
            .properties(properties)
            .packages("com.**.sec") //次数据源实体所在位置
            .persistenceUnit("SecPersistenceUnit")
            .build();
}
@Bean(name="transactionManagerSec")
public PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder) {
    return new JpaTransactionManager(entityManagerFactorySec(builder).getObject());
}

}
4.4 多数据源使用示例
多数据源配置时,在业务中使用时需要指定指向哪个数据源,示例如下:

package com.**;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import java.util.Map;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class DatasourceTest {

@Autowired
@Qualifier(value = "secJdbcTemplate")
private JdbcTemplate secJdbcTemplate;

//jdbc 查询 secJdbcTemplate 对应的数据源
@Test
public void t1(){
    String sql = "select * from sys_user_info";
    List<Map<String,Object>> list = secJdbcTemplate.queryForList(sql);
    for(Map<String,Object> m:list){
        System.out.println(m.get("username"));
    }
}

@Autowired
@Qualifier(value = "jdbcTemplate")
private JdbcTemplate priJdbcTemplate;

//jdbc 查询 jdbcTemplate 对应的数据源
@Test
public void t2(){
    String sql = "select * from sys_user_info";
    List<Map<String,Object>> list = priJdbcTemplate.queryForList(sql);
    for(Map<String,Object> m:list){
        System.out.println(m.get("username"));
    }
}

@PersistenceContext(unitName = "secPersistenceUnit")
private EntityManager secEntityManager;

//jpa 查询 secPersistenceUnit 对应的数据源
@Test
public void t3(){
    String sql = "select * from sys_user_info";
    List<Object[]> list = secEntityManager.createNativeQuery(sql).getResultList();
    for(Object[] m:list){
        System.out.println(m[0]);
    }
}

@PersistenceContext(unitName = "priPersistenceUnit")
private EntityManager entityManager;

//jpa 查询 priPersistenceUnit 对应的数据源
@Test
public void t4(){
    String sql = "select * from sys_user_info";
    List<Object[]> list = entityManager.createNativeQuery(sql).getResultList();
    for(Object[] m:list){
        System.out.println(m[0]);
    }
}

}

  1. 集成 Redis
    5.1 添加依赖

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>
5.2 application.yml 配置
spring:
redis:

host: 127.0.0.1 #服务器地址
password:  #redis 密码,没有时可不设置
port: 6379
jedis:
  pool: #连接池设置
    max-active: 8
    min-idle: 0
    max-idle: 8
    max-wait: -1
timeout:
database: 3 #使用的库,根据实际清空填写

5.3 Redis 工具类
下面的工具类是我在项目中用到的,因为只用到了字符串和哈希格式,所以并不全面,实际使用过程中,可根据实际情况修改。

package com.reach.dv.common.service;

import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**

  • @ClassName RedisTemplateService
  • @Date 2019/5/16
  • @Version 1.0
    **/

@Service
public class RedisTemplateService {

@Autowired
private StringRedisTemplate redisTemplate;

/**
 * 数据写入 redis
 *
 * @param key
 * @param value
 * @param <T>
 * @return
 */
public <T> boolean set(String key, T value) {
    String val = beanToString(value);
    if (val == null || val.length() <= 0) {
        return false;
    }
    redisTemplate.opsForValue().set(key, val);
    return true;
}

/**
 * 写入 hash 类型的数据
 * @param key
 * @param value
 * @return
 */
public boolean set(String key, Map<String,Object> value) {
    redisTemplate.opsForHash().putAll(key,value);
    return true;
}

/**
 * 数据写入 redis
 * @param key
 * @param value
 * @param timeout 超时时长(S) >0 时设置
 * @param <T>
 * @return
 */
public <T> boolean set(String key, T value,int timeout) {
    String val = beanToString(value);
    if (val == null || val.length() <= 0) {
        return false;
    }
    redisTemplate.opsForValue().set(key, val);
    if (timeout>0){
        redisTemplate.expire(key,timeout, TimeUnit.SECONDS);
    }
    return true;
}

private <T> String beanToString(T value) {
    String result = "";
    if (value == null) {
        return null;
    }
    Class clazz = value.getClass();
    if (clazz == int.class || clazz == Integer.class) {
        result = String.valueOf(value);
    } else if (clazz == long.class || clazz == Long.class) {
        result = String.valueOf(value);
    } else if (clazz == String.class) {
        result = String.valueOf(value);
    } else {
        result = JSON.toJSONString(value);
    }
    return result;
}

/**
 * 查询数据,返回序列化数据
 * @param key
 * @param <T>
 * @return
 */
public <T> T get(String key, Class<T> clazz) {
    String value = redisTemplate.opsForValue().get(key);
    if(key.equals("vin")){
        value = value.toUpperCase();
    }
    return stringToBean(value, clazz);
}

/**
 * 查询 hash 数据,返回序列化数据
 * @param key
 * @param mkey
 * @param clazz
 * @param <T>
 * @return
 */
public <T> T get(String key,String mkey, Class<T> clazz){
    String value = String.valueOf(redisTemplate.opsForHash().get(key,mkey));
    if(key.equals("vin")){
        value = value.toUpperCase();
    }
    return stringToBean(value, clazz);
}

private <T> T stringToBean(String value, Class<T> clazz) {
    T result;
    if (value == null || value.length() <= 0 || clazz == null) {
        return null;
    }
    if (clazz == int.class || clazz == Integer.class) {
        result = (T) Integer.valueOf(value);
    } else if (clazz == long.class || clazz == Long.class) {
        result = (T) Long.valueOf(value);
    } else if (clazz == String.class) {
        result = (T) value;
    } else {
        result = JSON.parseObject(value,clazz);
    }
    return result;
}

public boolean delete(String key){
    redisTemplate.delete(key);
    return true;
}

public Set<String> getKeys(String prefix){
    Set<String> keys = redisTemplate.keys(prefix);
    return keys;
}

/**
 * 判断 key 是否存在
 * @param key
 * @return
 */
public boolean hasKey(String key){
    return redisTemplate.hasKey(key);
}

/**
 * 更新 key 的过期时间
 * @param key
 * @param newTimeOut
 * @return
 */
public Boolean expireKey(String key,int newTimeOut){
    return redisTemplate.expire(key, newTimeOut, TimeUnit.SECONDS);
}

}
5.4 乱码处理
使用 RedisTemplate 进行数据缓存是会出现乱码现象,如下:

在这里插入图片描述

原因:Spring Boot 集成 Redis 时,自动配置了 RedisTemplate,但是 RedisTemlate 默认使用的是 JdkSerializationRedisSerializer,该序列化方式使用的二进制形式存储的数据,所以导致我们看到的代码是乱码的。

解决方法:手动配置 RedisTemplate,设置 key 的序列化为 StringRedisSerializer,设置 value 的序列化采用 Jackson2JsonRedisSerializer。重启 Spring Boot,执行存储操作,就可以得到正确的结果。

代码如下:

package com.reach.usercenter.conf;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.*;

/**

  • @ClassName CacheConfig
  • @Description
  • @Date 2019/5/17
  • @Version 1.0
    **/

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

private RedisSerializer<Object> valueSerializer() {
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);
    return jackson2JsonRedisSerializer;
}

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
    RedisSerializer<String> redisSerializer = new StringRedisSerializer();
    // 配置序列化
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
    RedisCacheConfiguration redisCacheConfiguration =
            config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer((valueSerializer())));

    RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
            .cacheDefaults(redisCacheConfiguration)
            .build();
    return cacheManager;
}

}

  1. 集成 Elasticsearch
    这里并没有使用 spring-boot-starter-data-elasticsearch,原因是 starter 中对应的 Elasticsearch 的版本最高是 6.x 的,如果我们使用的 ES 版本是 7.x,就会导致出错,所以使用了官方提供的 RestHighLevelClient。

个人感觉这是一款很好用的客户端,官方文档地址:

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html

6.1 添加依赖
<properties>

<java.version>1.8</java.version>
<elasticsearch.version>7.4.2</elasticsearch.version>

</properties>
<dependency>

<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${elasticsearch.version}</version>
<exclusions>
    <exclusion>
        <artifactId>elasticsearch</artifactId>
        <groupId>org.elasticsearch</groupId>
    </exclusion>
    <exclusion>
        <artifactId>elasticsearch-rest-client</artifactId>
        <groupId>org.elasticsearch.client</groupId>
    </exclusion>
</exclusions>

</dependency>
<dependency>

<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${elasticsearch.version}</version>

</dependency>
<dependency>

<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch.version}</version>

</dependency>
6.2 application.yml 配置
es:
host: ip1,ip2,ip3
port: 9200
client:

connectNum: 10
connectPerRoute: 50

6.3 ElasticSearch 配置
读取 application 配置文件中的 ES 的 host 和 port,工程启动时,初始化 RestHighLevelClient 和 RestClient,可以在业务模块直接调用 client。

ElasticConfig.java

package com.**;

import lombok.Getter;
import lombok.Setter;
import org.apache.http.HttpHost;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@Configuration
@ComponentScan(basePackageClasses = ElasticClientFactory.class)
public class ElasticConfig {

@Value("${es.host}")
private String host;

@Value("${es.port}")
private int port;

@Value("${es.client.connectNum}")
private Integer connectNum;

@Value("${es.client.connectPerRoute}")
private Integer connectPerRoute;

@PostConstruct
public void init(){
    System.setProperty("es.set.netty.runtime.available.processors", "false");
}

@Bean
public List<HttpHost> httpHost() {
    List<HttpHost> hosts = new ArrayList<HttpHost>();
    for (String h : host.split(",")) {
        hosts.add(new HttpHost(h, port, "http"));
    }
    return hosts;
}

@Bean(initMethod = "init", destroyMethod = "close")
public ElasticClientFactory getFactory() {
    ElasticClientFactory factory = new ElasticClientFactory(httpHost(), connectNum, connectPerRoute);
    return factory;
}

@Bean

// @Scope("singleton")

public RestClient getRestClient() {
    return getFactory().getClient();
}

@Bean

// @Scope("singleton")

public RestHighLevelClient getRHLClient() {
    return getFactory().getRhlClient();
}

}
ElasticClientFactory.java

package com.**;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.List;

@Slf4j
public class ElasticClientFactory {

public static int CONNECT_TIMEOUT_MILLIS = 1000;
public static int SOCKET_TIMEOUT_MILLIS = 60000;
public static int CONNECTION_REQUEST_TIMEOUT_MILLIS = 500;

private RestClient restClient;

private RestHighLevelClient restHighLevelClient;

private RestClientBuilder builder;

private List<HttpHost> httpHost;
private Integer connectNum;
private Integer connectPerRoute;

public ElasticClientFactory(List<HttpHost> httpHost, Integer connectNum, Integer connectPerRoute) {
    this.httpHost = httpHost;
    this.connectNum = connectNum;
    this.connectNum = connectPerRoute;
}

public RestClient getClient() {
    return restClient;
}

public RestHighLevelClient getRhlClient() {
    return restHighLevelClient;
}

private void init() {
    builder =RestClient.builder(httpHost.toArray(new HttpHost[httpHost.size()]));
    setConnectTimeOutConfig();
    restClient = builder.build();
    restHighLevelClient = new RestHighLevelClient(builder);
}

// 配置连接时间延时
public void setConnectTimeOutConfig() {
    builder.setRequestConfigCallback(requestConfigBuilder -> {
        requestConfigBuilder.setConnectTimeout(CONNECT_TIMEOUT_MILLIS);
        requestConfigBuilder.setSocketTimeout(SOCKET_TIMEOUT_MILLIS);
        requestConfigBuilder.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT_MILLIS);
        return requestConfigBuilder;
    });
}

private void close() {
    if (restClient != null) {
        try {
            restClient.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    log.info("Elastic Client Close ......");
}

}
6.4 示例
package com.**;

import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class ElasticTest {

@Autowired
private RestHighLevelClient client;

public void t(){
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("v").field("v").size(100000000);
    BoolQueryBuilder boolQueryBuilder = getBoolQuery();
    sourceBuilder.query(boolQueryBuilder).aggregation(termsAggregationBuilder);
    SearchResponse response = getSearchResponse(sourceBuilder);
    Aggregations aggregations = response.getAggregations();
    ParsedStringTerms termsCount = aggregations.get("v");
    List<ParsedStringTerms.ParsedBucket> list = (List<ParsedStringTerms.ParsedBucket>) termsCount.getBuckets();
    for (ParsedStringTerms.ParsedBucket buckets : list) {
        System.out.println(buckets.getKeyAsString());
    }
}

public BoolQueryBuilder getBoolQuery(){
    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    RangeQueryBuilder rangeQueryBuilder1 = QueryBuilders.rangeQuery("s").lt(255);
    RangeQueryBuilder rangeQueryBuilder2 = QueryBuilders.rangeQuery("v").lt(65535);
    RangeQueryBuilder rangeQueryBuilder3 = QueryBuilders.rangeQuery("c").lt(65535);
    RangeQueryBuilder rangeQueryBuilder4 = QueryBuilders.rangeQuery("i").lt(65535);
    RangeQueryBuilder rangeQueryBuilder5 = QueryBuilders.rangeQuery("ch").lt(65535);
    RangeQueryBuilder rangeQueryBuilder6 = QueryBuilders.rangeQuery("cl").lt(65535);
    boolQueryBuilder.must(rangeQueryBuilder1).must(rangeQueryBuilder2).must(rangeQueryBuilder3).must(rangeQueryBuilder4).must(rangeQueryBuilder5).must(rangeQueryBuilder6);
    return boolQueryBuilder;
}

public SearchResponse getSearchResponse(SearchSourceBuilder sourceBuilder) {
    String esIndex = "your index";
    SearchRequest searchRequest = new SearchRequest(esIndex);
    searchRequest.source(sourceBuilder);
    SearchResponse response = null;
    try {
        response = client.search(searchRequest, RequestOptions.DEFAULT);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return response;
}

}

  1. 集成 Spring Security+JWT
    7.1 添加依赖

<dependency>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>

</dependency>
7.2 创建用户表实体类(User.java)和 jpa 接口(UserRepository.java)
以下是本人项目中用到的 user,可以根据实际业务处理,下面的类中用到了 @Getter 和 @Setter 的注解,这是 Lombok 工具,需要引入Lombok 的 jar 包。

<dependency>

<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

</dependency>
使用 IDEA 时,需要安装 lombok 的插件,安装方法如下:

在这里插入图片描述

User.java

User 的实体类,并实现 UserDetails,根据数据库表结构配置此实体类。

package com.**;

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Table(name = "sys_user_info")
@Entity
@Getter
@Setter
public class User implements UserDetails {

@Id
@GeneratedValue
private Integer id;

@Column
private String username;

@Column
private String name;

@Column
private Integer type;

@Column
private String ip;

@Column
private String password;

@Column
private boolean enabled = true;

@Transient
private List<Integer> groups;

public List<Integer> getGroups() {
    return groups;
}

public void setGroups(List<Integer> groups) {
    this.groups = groups;
}

@LastModifiedDate
@Column
private Long updateTime = System.currentTimeMillis();

@Transient
private String time;

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

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

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

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

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> authorities = new ArrayList<>();

// for (Role role : roleList) {
// authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
// }

    return authorities;
}

private String mail;
private String phone;
private String i18n;
@Column(name = "data_source_id")
private String dataSourceId;

}
UserRepository.java

创建 User 实体的 JPA 查询接口,并添加一个通过用户名查询用户信息的方法。

package com.**;

import com.reach.usercenter.modules.login.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public interface UserRepository extends JpaRepository<User,Integer>, JpaSpecificationExecutor<User> {

User findByUsername(String s);

}
7.2 用户 VO(UserVO.java)
package com.**;

import com.**.User;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

@Getter
@Setter
public class UserVO extends User {

private String password;
private String username;
private String name;
private String groupId;
private String excMappingId;
private List<String> roleId;
private String ip;
private List<Map<String,Object>> datasource;
private String factory;
private String mail;
private String phone;
private String i18n;

}
7.3 登录成功后返回前端的用户信息(LoginSuccessVO.java)
package com.reach.usercenter.modules.login.dto;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Column;
import java.util.List;
import java.util.Map;

/**

  • @ClassName LoginSuccessVO
  • @Description
  • @Date 2019/5/22
  • @Version 1.0
    **/

@Getter
@Setter
public class LoginSuccessVO {

private Integer id;
private String user;
private String name;
private List<Map<String,Object>> datasource;
private String factory;
private String mail;
private String phone;
private String i18n;
private List<String> role;

}
7.4 自定义 UserDetailsService
自定义 CustomUserService,并实现 UserDetailsService,从数据库中获取用户进行身份验证。

package com.**;

import com.**.UserVO;
import com.**.UserRepository;
import com.**.User;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class CustomUserService implements UserDetailsService {

@Autowired
private UserRepository userRepository;

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
public UserVO loadUserByUsername(String s) throws UsernameNotFoundException {
    User user = userRepository.findByUsername(s);
    if(user==null){
        return new UserVO();
    }
    UserVO vo = getUserDetails(user);
    return vo;
}

public UserVO getUserDetails(User user){
    UserVO userDTO = new UserVO();
    BeanUtils.copyProperties(user,userDTO);

    //TODO 实际业务逻辑代码
    //根据需要返回给前端和实际业务处理中用到的用户信息,加工业务逻辑代码,把信息写入到 UserVO 中
    ......
    return userDTO;
}

}
7.5 自定义 AuthenticationSuccessHandler
这里用到了之前的Redis 的设置和Jasypt 加密设置,把登录成功的用户的信息进行加密处理,并存储到 Redis 中,每次登录成功后,不用再次去查询数据库中的用户信息了,加快请求速度。

package com.**.handler;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.**.service.RedisTemplateService;
import com.**.LoginSuccessVO;
import com.**.UserVO;
import com.**.JwtTokenUtil;
import com.**.Response;
import com.**.ResponseUtil;
import org.jasypt.encryption.StringEncryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

/**

  • 处理登录验证成功类
    */

@Component
public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {

private Logger logger = LoggerFactory.getLogger(CustomAuthSuccessHandler.class);

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Autowired
private ObjectMapper objectMapper;

@Autowired
private RedisTemplateService redisTemplate;

@Autowired
private StringEncryptor stringEncryptor;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
    UserVO userDetails = (UserVO) authentication.getPrincipal();
    Response response = new Response();
    LoginSuccessVO vo = new LoginSuccessVO();
    BeanUtils.copyProperties(userDetails,vo);
    vo.setUser(userDetails.getUsername());
    vo.setId(userDetails.getId());
    vo.setRole(userDetails.getRoleId());
    logger.info(userDetails.getUsername()+"登录成功");
    int tokenDuration = jwtTokenUtil.getTokenDuration(); //token 有效时长
    String token = jwtTokenUtil.generateToken(userDetails.getUsername(),tokenDuration);
    logger.info("登录生成的 Token:{}",token);
    response = ResponseUtil.success(vo,token);
    //token 写入缓存
    String userEncrypt = stringEncryptor.encrypt(JSON.toJSONString(userDetails));
    redisTemplate.set(token,userEncrypt,tokenDuration);
    httpServletResponse.setContentType("application/json;charset=UTF-8");
   httpServletResponse.getWriter().append(objectMapper.writeValueAsString(response));
}

}
7.6 自定义 AuthenticationFailureHandler
自定义 CustomAuthFailureHandler 并实现 AuthenticationFailureHandler,用于处理登录验证失败后的处理,在该类中通过返回的异常类型,返回给前端验证失败的的原因。

package com.**.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.**.Response;
import com.**.ResponseUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

/**

  • 处理登录验证失败类
    */

@Component
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {

private Logger logger = LoggerFactory.getLogger(CustomAuthFailureHandler.class);

@Autowired
private ObjectMapper objectMapper;

@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
    StringBuffer msg = new StringBuffer();
    if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
        msg.append("用户名或密码输入错误,登录失败!");
    } else if (e instanceof DisabledException) {
        msg.append("账户被禁用,登录失败,请联系管理员!");
    } else {
        msg.append("登录失败!");
    }
    Response response = ResponseUtil.error(1,msg.toString());
    httpServletResponse.setContentType("application/json;charset=UTF-8");
   httpServletResponse.getWriter().append(objectMapper.writeValueAsString(response));
}

}
7.7 自定义 LogoutSuccessHandler
自定义 CustomLogoutSuccessHandler,并实现 LogoutSuccessHandler,用于处理用户注销时的操作。这里发起注销请求后,通过请求 header 中的 token 信息,删除存储在 Redis 中的 token 的信息,释放缓存。

package com.**.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.**.RedisTemplateService;
import com.**.Response;
import com.**.ResponseUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

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

/**

  • 自定义注销成功
    */

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

private Logger logger = LoggerFactory.getLogger(CustomLogoutSuccessHandler.class);

@Autowired
private ObjectMapper objectMapper;

@Autowired
private RedisTemplateService redisTemplate;

@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
    String authHeader = httpServletRequest.getHeader("Authorization");
    String authToken;
    if (authHeader.contains("Bearer ")) {
        authToken = authHeader.substring(7);
    } else {
        authToken = authHeader;
    }
    redisTemplate.delete(authToken);
    Response response = ResponseUtil.success();
    httpServletResponse.setContentType("application/json;charset=UTF-8");
   httpServletResponse.getWriter().append(objectMapper.writeValueAsString(response));
}

}
7.8 自定义 AuthenticationEntryPoint
自定义 CustomAuthenticationEntryPoint 实现 AuthenticationEntryPoint,用来解决匿名用户访问无权限资源时的异常,并把异常信息返回给前端。

package com.**.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.**.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Autowired
private ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
    Response response = new Response();
    response.setCode(300);
    response.setMsg("未登陆");

    httpServletResponse.setContentType("application/json;charset=UTF-8");
    httpServletResponse.setStatus(403);
   httpServletResponse.getWriter().append(objectMapper.writeValueAsString(response));
}

}
7.9 自定义 AccessDeniedHandler
自定义 CustomAccessDeniedHandler 并实现 AccessDeniedHandler,用来解决认证过的用户访问无权限资源时的异常,并把异常信息返回给前端。

package com.**.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.**.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**

  • @Description 访问拒绝处理
    **/

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

@Autowired
private ObjectMapper objectMapper;

@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
    Response response = new Response();
    response.setCode(403);
    response.setMsg("权限不足");

    httpServletResponse.setContentType("application/json;charset=UTF-8");
   httpServletResponse.getWriter().append(objectMapper.writeValueAsString(response));
}

}
7.10 JWT 拦截
这里用到了之前的Redis 的设置和Jasypt 加密设置,请求验证通过后,从 Redis 里读取用户的加密信息,解密后把信息放入 HttpServletRequest,用于业务处理时使用。

JwtTokenUtil.java

Jwt 的工具类,提供了生成 token 的公共方法。

package com.**.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;

@Service
public class JwtTokenUtil {

private static Key key = new SecretKeySpec(new BCryptPasswordEncoder().encode("study-test").getBytes(),SignatureAlgorithm.HS512.getJcaName());

@Value("${server.tokenDuration}")
private int tokenDuration;

public int getTokenDuration() {
    return tokenDuration;
}

/**
 * 生成 token
 * @param username
 * @param tokenDuration 有效时长
 * @return
 */
public String generateToken(String username,int tokenDuration ) {

    String token =Jwts.builder()
            .setClaims(null)
            .setSubject(username)
            .setExpiration(new Date(System.currentTimeMillis() + tokenDuration * 1000))
            .signWith(SignatureAlgorithm.HS512,key)
            .compact() ;
    return token;
}

/**
 * 解析 token,如果已经过期,刷新 token
 * @param token
 * @return
 */
public String parseToken(String token) {
    String subject = null;
    try {
        Claims claims = Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(token).getBody();
        subject = claims.getSubject();
    } catch (Exception e) {
        System.out.println(e.getLocalizedMessage());
        subject="taken 无效";
    }
    return subject;
}

}
JwtAuthenticationTokenFilter.java

JWT 全局拦截,对所有的 API 请求进行拦截,从请求 header 中获取 token 信息,并与 Redis 中的缓存信息进行比对验证,验证通过后,把 Redis 中的用户信息进行解密,并放入 HttpServletRequest 中,用于服务接口中使用。

package com.**.filter;

import com.alibaba.fastjson.JSON;
import com.**.RedisTemplateService;
import com.**.UserVO;
import com.**.JwtTokenUtil;
import org.jasypt.encryption.StringEncryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Autowired
private RedisTemplateService redisTemplate;

@Autowired
private StringEncryptor stringEncryptor;

@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException {
    String authHeader = req.getHeader("Authorization");
    if (authHeader != null) {
        String authToken;
        if (authHeader.contains("Bearer ")) {
            authToken = authHeader.substring(7);
        } else {
            authToken = authHeader;
        }
        boolean flag = redisTemplate.hasKey(authToken);
        if (flag) {
            int tokenDuration = jwtTokenUtil.getTokenDuration(); //token 有效时长
            boolean expireFlag = redisTemplate.expireKey(authToken, tokenDuration);
            if (expireFlag) {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                Date date = new Date();
                String dd = sdf.format(date);
                logger.info("token 有效时长更新成功,更新时间为:{}",dd);
            } else {
                logger.info("token 有效时长更新失败");
            }
            String userInfo = stringEncryptor.decrypt(redisTemplate.get(authToken, String.class));
            if (SecurityContextHolder.getContext().getAuthentication() == null) {
                UserVO userDetails = JSON.parseObject(userInfo,UserVO.class);
                if (userDetails != null) {
                    req.setAttribute("user", userDetails);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
    }
    chain.doFilter(req, res);
}

}
7.11 定义权限验证 RabcAuthorityService
对请求 API 进行拦截,与当前用户的角色进行对比,判断当前登录用户是否有权限访问该 API。

package com.**.filter;

import com.reach.usercenter.common.service.RedisTemplateService;
import com.reach.usercenter.modules.login.dto.UserVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Component("rabcservice")
public class RabcAuthorityService {

@Autowired
private RedisTemplateService redisTemplate;

public boolean hasPermession(HttpServletRequest request, Authentication authentication) {

    String path = request.getServletPath();
    Object userInfo = authentication.getPrincipal();
    boolean hasPermission = false;
    if(path.equals("/login")){
        hasPermission = true;
    } else if (path.equals("/logout")){
        String authToken = request.getHeader("Authorization");
        redisTemplate.delete(authToken);
    }else{
        if (userInfo instanceof UserVO) {
            hasPermission = true;
        }
    }
    return hasPermission;
}

// /**
// * 根据用户拥有的角色获取可访问菜单
// *
// * @return
// */
// public List<String> getRoleMenus(List<UserRoleDTO> userRoles) {
// List<String> allMenu = new ArrayList<>();
// for (UserRoleDTO userRoleDTO : userRoles) {
// List<String> menus = redisTemplate.get("role_"+userRoleDTO.getRoleId(),List.class);
// menus.stream().forEach(menu -> allMenu.add(menu));
// }
// return allMenu;
// }
}
7.12 配置 WebSecurityConfig
在下边的配置文件中,对登录请求的验证进行配置,并指定请求的白名单,对白名单中的请求不进行拦截。

package com.**.conf;

import com.**.filter.JwtAuthenticationTokenFilter;
import com.*.handler.;
import com.**.CustomUserService;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
CustomUserService userService;

@Autowired
CustomAuthSuccessHandler authSuccessHandler;

@Autowired
CustomAuthFailureHandler authFailureHandler;

@Autowired
CustomLogoutSuccessHandler logoutSuccessHandler;

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;

@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

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

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}

//白名单
@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/ext/**")
            .antMatchers("/es/**")
            .antMatchers("/webjars/**")
            .antMatchers("/swagger-resources/**")
            .antMatchers("/v2/**")
            .antMatchers("/swagger-ui.html","/index.html","/static/**","/doc.html","/docs.html");
}

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

    http.cors().and().csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .httpBasic().authenticationEntryPoint(authenticationEntryPoint)
            .and()
            .authorizeRequests()
            .anyRequest()
            .access("@rabcservice.hasPermession(request,authentication)")

// .authenticated()

            .and()
            .formLogin()
            .loginPage("/login_page")
            .successHandler(authSuccessHandler)
            .failureHandler(authFailureHandler)
            .loginProcessingUrl("/login")
            .usernameParameter("username")
            .passwordParameter("password")
            .permitAll()
            .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(logoutSuccessHandler)
            .permitAll();

    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);// 无权访问
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

}

}
通过以上代码就完成了 Spring Boot 集成 Spring Security 的工作,实际项目中可以根据实际的业务需求,对该模块进行修改,以适应具体要求。

  1. 跨域设置
    为了实现访问网络的安全,浏览器提供了同源策略来保持安全,不同源的客户端在没有明确授权的情况下,是不能读写对方的资源。

同源:请求地址中协议、域名和端口号必须完全相同。

前后端分离时,受浏览器同源策略的影响,必须进行跨域配置。下边的示例是通过配置实现全局跨域配置,使所有的接口支持跨域配置。

package com...conf;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig {

@Bean
public WebMvcConfigurer corsConfigurer(){
    return new WebMvcConfigurer() {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowCredentials(true)
                    .allowedHeaders("*")
                    .allowedMethods("*")
                    .allowedOrigins("*")
                    .maxAge(3600);
        }
    };
}

}

Last modification:May 13th, 2020 at 10:50 pm
如果觉得我的文章对你有用,请随意赞赏