Spring Security集成spring-session(八)

  • 添加依赖 pom.xml
<!-- 使用 spring-session-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • 配置文件 application.yml
spring:
  session:
    store-type: redis
    redis:
      namespace: spring:session
      flush-mode: on_save
  redis:
    host: localhost
    database: 0
    port: 6379

SecurityContextPersistenceFilter.java 过滤器中 HttpSessionSecurityContextRepository 将会从redis中获取信息。

sharp-database中的BaseDAOImpl实体中自定义属性序列化

环境搭建

属性类

PhoneNumber

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PhoneNumber {

    private String code;

    private String number;

    /**
     * 序列化格式
     * @return
     */
    @Override
    public String toString() {
        return code + "-" + number;
    }
}

实体类

@Data
@ToString
@Builder
@TableName("t_project")
@NoArgsConstructor
@AllArgsConstructor
public class Project {
    @Id
    private Long id;

    private PhoneNumber phoneNumber;

反序列化转换

@Component
final public class StringToPhoneNumberConverterFactory implements ConverterFactory<String, PhoneNumber> {

    @Override
    public <T extends PhoneNumber> Converter<String, T> getConverter(Class<T> aClass) {
        return new StringToPhoneNumber();
    }

    private static class StringToPhoneNumber<T> implements Converter<String, T> {

        @Override
        public T convert(String source) {
            if (StringUtils.isBlank(source)) {
                return null;
            }

            String[] arr = source.split("-");
            return (T) PhoneNumber.builder().code(arr[0]).number(arr[1]).build();
        }
    }

}

测试

@Test
public void testSave() {
    Project project = new Project();

    project.setPhoneNumber("861-18888888888");

    projectDAO.insert(project);
    Assert.assertNotNull(project.getId());
}

@Test
public void testFindById() {
    Optional<Project> optional = projectDAO.selectById(479411923147194368L);
    Project project = optional.get();

    Assert.assertEquals("18888888888", project.getPhoneNumber().getNumber());
}

sharp-database中的BaseDAOImpl实体中对Enum属性的支持

环境搭建

枚举类

StatusEnum code是String类型

@AllArgsConstructor
@Getter
public enum StatusEnum {
    DEFAULT("DEFAULT");

    @JsonValue
    public String getCode() {
        return this.name();
    }

    private final String label;

    public static StatusEnum valueOfCode(String code) {
        return valueOf(code);
    }
}

SexEnum code是int类型

@AllArgsConstructor
@Getter
public enum SexEnum {
    UNKNOWN(0, "Unknown"),
    MALE(1, "Male"),
    FEMALE(2, "Female");

    private static final Map<Integer, SexEnum> codeMap = new HashMap<>();

    static {
        for (SexEnum e : values()) {
            codeMap.put(e.code, e);
        }
    }

    private final int code;

    private final String label;

    @JsonValue
    public int getCode() {
        return this.code;
    }

    public static SexEnum valueOfCode(int code) {
        return codeMap.get(code);
    }
}

枚举中必须包含静态方法 valueOfCode 和 方法 getCode

实体类

@Data
@ToString
@Builder
@TableName("t_project")
@NoArgsConstructor
@AllArgsConstructor
public class Project {
    @Id
    private Long id;

    private SexEnum sex;

    private StatusEnum status;

测试

@Test
public void testSave() {
    Project project = new Project();

    project.setSex(SexEnum.FEMALE);
    project.setStatus(StatusEnum.DEFAULT)
    projectDAO.insert(project);
    Assert.assertNotNull(project.getId());
}

@Test
public void testFindById() {
    Optional<Project> optional = projectDAO.selectById(479411923147194368L);
    Project project = optional.get();

    Assert.assertEquals(SexEnum.FEMALE, project.getSex());
    Assert.assertEquals(SexEnum.DEFAULT project.getStatus());
}

sharp-database中的BaseDAOImpl实体中有POJO属性/List POJO/List Map使用Json存储

环境准备

Address.java

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address implements JsonStringToObjectConverterFactory.JsonValue {

    private String code;

    private String detail;

}
  • 实现接口 JsonStringToObjectConverterFactory.JsonValue 表示反序列化的时候,底层使用JsonUtils.toObject()

Project.java

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Project {

    @Id
    private Long id;

    private Address address;

    private List<Address> list;

}

测试

@Test
public void testSave() {
    Project project = new Project();

    project.setAddress(Address.builder().code("001").detail("苏州").build());
    project.setList(Lists.newArrayList(Address.builder().code("001").detail("苏州").build()));

    projectDAO.insert(project);
    Assert.assertNotNull(project.getId());
}

@Test
 public void testFindById() {
     Optional<Project> optional = projectDAO.selectById(479411923147194368L);
     Project project = optional.get();
     Assert.assertEquals("001", project.getAddress().getCode());
     Assert.assertEquals("001", project.getList().get(0).getCode());
}

Spring Security Oauth2 resource-server鉴权(七)

说明

spring-security-oauth 这个项目不赞成使用了。oauth2已经由Spring Security提供服务。Spring Security没有提供对认证服务器的支持,需要 spring-authorization-server 去支持。
https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

The Spring Security OAuth project is deprecated. The latest OAuth 2.0 support is provided by Spring Security. See the OAuth 2.0 Migration Guide for further details.

Since Spring Security doesn’t provide Authorization Server support, migrating a Spring Security OAuth Authorization Server see https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

配置

添加依赖 build.gradle

implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server"
implementation 'org.springframework.boot:spring-boot-starter-web'

配置文件 application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          public-key-location: classpath:key.public
          jws-algorithm: RS512

配置类 ResourceServerConfig.java

@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(a -> a
                    .antMatchers("/public", "/error", "/webjars/**").permitAll()
                    .antMatchers("/admin").hasAnyRole("admin")
                    .anyRequest().authenticated()
            )
            .oauth2ResourceServer()
            .jwt();
    }

}
  • /public 是不需要认证就可以访问的
  • /admin 是需要admin角色才能访问的
  • /user 是需要user角色才能访问的
  • /index 是需要认证的

工具类

生成私钥和公钥

@UtilityClass
@Slf4j
public class SecurityUtils {

    /**
     * 私钥
     */
    private static final RSAPrivateKey PRIVATE_KEY = RsaKeyConverters.pkcs8().convert(SecurityUtils.class.getResourceAsStream("/key.private"));

    /**
     * 公钥
     */
    private static final RSAPublicKey PUBLIC_KEY = RsaKeyConverters.x509().convert(SecurityUtils.class.getResourceAsStream("/key.public"));


    /**
     * rsa算法加解密时的填充方式
     */
    private static final String RSA_PADDING = "RSA/ECB/PKCS1Padding";

    /**
     * 生成私钥和公钥
     */
    public static void keys() {
        try {
            KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
            keyPairGen.initialize(2048);
            KeyPair keyPair = keyPairGen.generateKeyPair();
            PrivateKey privateKey = keyPair.getPrivate();
            PublicKey publicKey = keyPair.getPublic();
            log.info("{}{}{}", "\n-----BEGIN PRIVATE KEY-----\n", Base64.getMimeEncoder().encodeToString(privateKey.getEncoded()), "\n-----END PRIVATE KEY-----");
            log.info("{}{}{}", "\n-----BEGIN PUBLIC KEY-----\n", Base64.getMimeEncoder().encodeToString(publicKey.getEncoded()), "\n-----END PUBLIC KEY-----");
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * 加密
     *
     * @param plaintext 明文
     * @return 密文
     */
    private static String encrypt(String plaintext) {
        try {
            Cipher cipher = Cipher.getInstance(RSA_PADDING);
            cipher.init(Cipher.ENCRYPT_MODE, PUBLIC_KEY);
            String encrypt = Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes()));
            log.info("The plaintext {} is encrypted as: {}", plaintext, encrypt);
            return encrypt;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * 解密
     *
     * @param cipherText 密文
     * @return 明文
     */
    private static String decrypt(String cipherText) {
        try {
            Cipher cipher = Cipher.getInstance(RSA_PADDING);
            cipher.init(Cipher.DECRYPT_MODE, PRIVATE_KEY);
            String decrypt = new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)));
            log.info("The ciphertext {} is decrypted as: {}", cipherText, decrypt);
            return decrypt;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    public static void main(String[] args) {
        keys();
    }
}

将文件 key.privatekey.public 放到resources目录下

生成token

public final class JwtUtils {

    /**
     * 私钥
     */
    private static final RSAPrivateKey PRIVATE_KEY = RsaKeyConverters.pkcs8().convert(JwtUtils.class.getResourceAsStream("/key.private"));


    private JwtUtils() {}

    /**
     * 生成jwt
     *
     * @return jwt
     */
    public static String jwt(JWTClaimsSet claimsSet) {
        try {
            SignedJWT jwt = new SignedJWT(new JWSHeader(new JWSAlgorithm("RS512")), claimsSet);
            // 私钥签名,公钥验签
            jwt.sign(new RSASSASigner(PRIVATE_KEY));
            return jwt.serialize();
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    public static void main(String[] args) {
        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
                .subject("RS512 Rick")
                .issuer("https://xhope.top")
                .claim("scope", "user")
                .build();

        String jwtToken = JwtUtils.jwt(claimsSet);
        System.out.println(jwtToken);
    }

}

测试接口

@RestController
public class ResourceController {

    /**
     * 403没有访问权限
     * @return
     */
    @GetMapping(value = "admin")
    public String admin() {
        return "admin";
    }

    @PreAuthorize("hasAnyAuthority('ROLE_USER, SCOPE_user')")
    @GetMapping(value = "user")
    public String user() {
        return "user";
    }

    /**
     * 不需要认证就能访问
     * @return
     */
    @GetMapping(value = "public")
    public String publicFun() {
        return "public";
    }
}

利用 postman 进行测试,将token放到header中

curl -X GET \
  http://localhost:8080/admin \
  -H 'Authorization: Bearer eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczpcL1wveGhvcGUudG9wIiwic3ViIjoiUlM1MTIgUmljayIsInNjb3BlIjoibWVzc2FnZS5yZWFkIG1lc3NhZ2Uud3JpdGUifQ.buy_qLLpLodfEwKwRatnHZctZv7pYrgaiX7gjC79tA5ZQiEI_zpO7IvPE_Pw3CSBBZ7Jfz90y1gIq85RK8pAVbIceARsvVK2t8wGq5N6L6jwmi9drkvEMEIdxIijVYfNH7EXakAqx3aN8siScXWX4VTYaSuSd0LFrzQiV2HDmBd0FMGH2OXJmebnD2HI-zXtp02isUTVLReF13DZWV4cG_sr2aix0BjkSl6fhXu7SLZnJTE0yHI47Sc68O6w6J5rqpYUfD4WtM_C9go3iyzldN4oVh67HvzEaJ62ZIx2sKjTITLE_quISxYEnYc62oR1hL87JkGayi7JFl1Sl6o9BA'

如果通过参数传递token,需要修改

@Override
protected void configure(HttpSecurity http) throws Exception {
    DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
    // 允许参数access_token
    resolver.setAllowUriQueryParameter(true);

    http
        .authorizeRequests(a -> a
                .antMatchers("/public", "/error", "/webjars/**").permitAll()
                .antMatchers("/admin").hasAnyRole("admin")
                .anyRequest().authenticated()
        )
        .oauth2ResourceServer()
            .bearerTokenResolver(resolver)
        .jwt();
}

利用浏览器进行测试,将token放到参数access_token中

curl -X GET \
  'http://localhost:8080/user?access_token=eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczpcL1wveGhvcGUudG9wIiwic3ViIjoiUlM1MTIgUmljayIsInNjb3BlIjoibWVzc2FnZS5yZWFkIG1lc3NhZ2Uud3JpdGUifQ.buy_qLLpLodfEwKwRatnHZctZv7pYrgaiX7gjC79tA5ZQiEI_zpO7IvPE_Pw3CSBBZ7Jfz90y1gIq85RK8pAVbIceARsvVK2t8wGq5N6L6jwmi9drkvEMEIdxIijVYfNH7EXakAqx3aN8siScXWX4VTYaSuSd0LFrzQiV2HDmBd0FMGH2OXJmebnD2HI-zXtp02isUTVLReF13DZWV4cG_sr2aix0BjkSl6fhXu7SLZnJTE0yHI47Sc68O6w6J5rqpYUfD4WtM_C9go3iyzldN4oVh67HvzEaJ62ZIx2sKjTITLE_quISxYEnYc62oR1hL87JkGayi7JFl1Sl6o9BA'

源码分析

BearerTokenAuthenticationFilter.java

  • 获取token:从header中获取name是 Authorization 的值, 判断是否是以 Bearer 开头,如果是,那么解析出token值;否则如果允许参数传递token,则尝试从参数access_token中解析token。
  • 验证token:将值包装成 BearerTokenAuthenticationToken,交由 AuthenticationManager,最终由 JwtAuthenticationProvider 进行验证。
  • 底层解析token:NimbusJwtDecoder 进行decode;如果是 SignedJWT 使用公钥(yml中配置的公钥,项目启动的时候就会读取公钥信息)验签;验证成功后token验证由 DelegatingOAuth2TokenValidator 代理去进行其他验证。JwtTimestampValidator 验证日期是否过期。

OAuth2ResourceServerJwtConfiguration.java

参考阅读: