Spring Security Oauth2密码登录(九)

说明

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

虽然已经不推荐使用,但是很多的老项目还是使用的Spring Security Oauth,所以还是很有必要学习一下的。

环境搭建

  • 添加依赖
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.5.5</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 授权服务 AuthorizationServer.java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .tokenKeyAccess("permitAll()") // oauth/token_key是公开
                .checkTokenAccess("permitAll()") // oauth/check_token公开
                .allowFormAuthenticationForClients() // 密码模式:表单认证(申请令牌)
        ;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()// 使用in-memory存储
                .withClient("c")// client_id
                .secret("123456")//客户端密钥
                .resourceIds("res")//资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //加上验证回调地址
                .redirectUris("http://www.baidu.com");
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)// 认证管理器 => 密码模式需要在认证服务器中设置 中配置AuthenticationManager
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    @Bean
    public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder) {
        List<AuthenticationProvider> providers = new ArrayList<>();
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService(passwordEncoder));
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        providers.add(daoAuthenticationProvider);
        return new ProviderManager(providers);
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        //Admin Role
        UserDetails theUser = User.withUsername("rick")
                .password(passwordEncoder.encode("123456"))
                .roles("ADMIN").build();
        //User Role
        UserDetails theManager = User.withUsername("john")
                .password(passwordEncoder.encode("123456"))
                .roles("USER").build();
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(theUser);
        userDetailsManager.createUser(theManager);
        return userDetailsManager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}
  • 资源服务配置 ResourceServer.java
@Configuration
@EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 资源id
        resources.resourceId("res")
                .stateless(true)
                .accessDeniedHandler((request, response, e) -> {
                    response.getWriter().write(e.getMessage());
                });
    }


    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}
  • 接口 IndexController.java
@RestController
public class IndexController {

    @GetMapping
    public String index() {
        return "index";
    }
}

测试

  • Postman请求
http://localhost:8080/oauth/token \
        -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
        -F grant_type=password \
        -F client_id=c \
        -F client_secret=123456 \
        -F scope=all \
        -F username=rick \
        -F password=123456

org.springframework.security.oauth2.provider.endpoint.TokenEndpoint 处理请求。

  • 响应返回token
{
    "access_token": "1QRBR3bR2xY0hq7zPIrGoXhuz8Q",
    "token_type": "bearer",
    "refresh_token": "R85zWIbZJUIlY5dOco2NuR-sboM",
    "expires_in": 43199,
    "scope": "all"
}
  • 请求受保护资源

header中添加参数 Authorization

curl -X GET \
  http://localhost:8080/ \
  -H 'Authorization: Bearer 1QRBR3bR2xY0hq7zPIrGoXhuz8Q' 

请求参数添加 access_token

curl -X GET \
  'http://localhost:8080/?access_token=1QRBR3bR2xY0hq7zPIrGoXhuz8Q'

参考链接

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());
}