当前位置:   article > 正文

实践指南:使用SpringBoot构建API后端服务

spring boot 做纯api服务

#介绍

这个指南非常简单,不是从入门到进阶,不会面面俱到的讲如何从零起步,覆盖所有知识点。只是通过构建了一个小后端API工程,实现了基本功能。 这个指南中包含了Spring boot, Spring data, Spring Security的配置使用及单元测试的创建。

#Spring Boot简单配置

Spring Boot中提供了profile的概念,通过profile可以选择性的使用或者不适用应用程序的某些部分。

通过profile来选择使用一个class

  1. @Component
  2. @Profile("dev")
  3. public class LoggingEmailGateway implements EmailGateway {
  4. ...
  5. }

上面的代码中,@Profile注解标注了这个class的profile是“dev”,那么, 在application.properties中使用spring.profiles.active=dev 或者在命令行中使用这个参数来激活这个profile: java -jar application.jar --spring.profiles.active=dev

某个profile专用的一组配置

默认情况下,spring boot是会把所有的配置放在application.properties中配置。如果需要为某个profile配置专用的一组配置内容,可以使用命名规范符合application-<profileName>.properties的配置文件来实现:

application-prod.properties

server.port = 5000

配置日志

spring boot默认使用logback来记录日志,把日志内容输出到控制台上。

生产环境的日志配置

我们需要把生产环境的日志输出到文件中。这需要在配置文件中实现这个配置。在src/main/resources下面创建logback-spring.xml文件:

logback-spring.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration>
  3. <!-- spring boot默认的logback配置 -->
  4. <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
  5. <!-- dev和local这两个profile的配置,日志输出到控制台 -->
  6. <springProfile name="dev,local">
  7. <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
  8. <root level="INFO">
  9. <appender-ref ref="CONSOLE" />
  10. </root>
  11. </springProfile>
  12. <!-- staging和prod这两个profile的配置,日志输出到文件中-->
  13. <springProfile name="staging,prod">
  14. <include resource="org/springframework/boot/logging/logback/file-appender.xml"/>
  15. <root level="INFO">
  16. <appender-ref ref="FILE"/>
  17. </root>
  18. </springProfile>
  19. </configuration>

要指定日志文件名,需要修改application-staging.propertiesapplication-prod.proerties 文件:

  1. logging.file=babyrabbit.log
  2. logging.level.root=INFO

测试环境的日志配置

要为测试环境配置日志,可以在src/test/resources目录下创建logback-test.xml。在运行测试时,这个文件会被放在classpath的前端,并被LogBack自动使用。 这个文件很简单,就是把调试日志输出到控制台:

  1. <configuration>
  2. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  3. <encoder>
  4. <pattern>%date{YYYY-MM-dd HH:mm:ss} %level [%thread] %logger{0} - %msg%n%ex</pattern>
  5. </encoder>
  6. </appender>
  7. <root level="WARN">
  8. <appender-ref ref="STDOUT"/>
  9. </root>
  10. <!-- 输出指定包的调试日志 -->
  11. <logger name="com.springbook.application">
  12. <level value="DEBUG"/>
  13. </logger>
  14. <logger name="org.hibernate">
  15. <level value="WARN"/>
  16. </logger>
  17. <logger name="org.hibernate.type">
  18. <level value="WARN"/> <!-- set to TRACE to view parameter binding in queries -->
  19. </logger>
  20. <logger name="org.springframework.security">
  21. <level value="WARN"/>
  22. </logger>
  23. </configuration>

在执行命令 mvn clean install 编译程序的时候,就会运行测试代码并在控制台输出调试日志。 如果不想在编译的时候看调试日志,可以让maven把所有的输出写入到一个文件里,这需要在pom.xml中配置surefire插件:

  1. <plugin>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-surefire-plugin</artifactId>
  4. <configuration>
  5. <redirectTestOutputToFile>true</redirectTestOutputToFile>
  6. <printSummary>false</printSummary>
  7. </configuration>
  8. </plugin>

这个插件将把每个测试的输出写入到一个独立的文件中。

Demo工程

添加依赖组件

指南的工程中需要使用jpa, spring security, h2 database, sprint boot test, spring security test,lombok,mockmvc,google guava,因此,工程的pom.xml 中需要添加如下依赖:

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-jpa</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-security</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-starter-web</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>com.h2database</groupId>
  16. <artifactId>h2</artifactId>
  17. <scope>runtime</scope>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-configuration-processor</artifactId>
  22. <optional>true</optional>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.projectlombok</groupId>
  26. <artifactId>lombok</artifactId>
  27. <optional>true</optional>
  28. </dependency>
  29. <dependency>
  30. <groupId>org.springframework.boot</groupId>
  31. <artifactId>spring-boot-starter-test</artifactId>
  32. <scope>test</scope>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.springframework.restdocs</groupId>
  36. <artifactId>spring-restdocs-mockmvc</artifactId>
  37. <scope>test</scope>
  38. </dependency>
  39. <dependency>
  40. <groupId>org.springframework.security</groupId>
  41. <artifactId>spring-security-test</artifactId>
  42. <scope>test</scope>
  43. </dependency>
  44. <dependency>
  45. <groupId>com.google.guava</groupId>
  46. <artifactId>guava</artifactId>
  47. <version>18.0</version>
  48. </dependency>
  49. </dependencies>

用户管理

这个工程首先要做的是构建用户管理和安全。

首先创建User领域类:

User.java

  1. package com.babyrabbit.demo.user;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import java.util.Set;
  5. import java.util.UUID;
  6. /**
  7. * Created by kevin on 2018/9/13.
  8. */
  9. @Data
  10. @AllArgsConstructor
  11. public class User {
  12. private UUID id;
  13. private String email;
  14. private String password;
  15. private Set<UserRole> roles;
  16. }

UserRole类是定义用户的角色:

UserRole.java

  1. package com.babyrabbit.demo.user;
  2. /**
  3. * Created by kevin on 2018/9/13.
  4. */
  5. public enum UserRole {
  6. OFFICER,CAPTAIN,ADMIN
  7. }

通常我们会把用户信息存储在数据库里,这首先要根据JPA规范把User添加上数据库存储信息:

User.java

  1. package com.babyrabbit.demo.user;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. import javax.persistence.*;
  6. import javax.validation.constraints.NotNull;
  7. import java.util.Set;
  8. import java.util.UUID;
  9. /**
  10. * Created by kevin on 2018/9/13.
  11. */
  12. @Data
  13. @AllArgsConstructor
  14. @NoArgsConstructor
  15. @Entity
  16. @Table(name="tb_user")
  17. public class User {
  18. @Id
  19. private UUID id;
  20. private String email;
  21. private String password;
  22. @ElementCollection(fetch = FetchType.EAGER)
  23. @Enumerated(EnumType.STRING)
  24. @NotNull
  25. private Set<UserRole> roles;
  26. }

在这个新版本的User类中,有下面几点变化:

  • 使用@Entity和@Table将User类变成JPA实体
  • 使用@Id将id字段标注为主键
  • 由于jpa要求所有的实体对象有无参数的构造方法,在类上添加了@NoArgsConstructor自动生成无参数的构造方法

User仓库

要存储User,需要使用存储仓库。

UserRepository.java

  1. package com.babyrabbit.demo.user;
  2. import org.springframework.data.repository.CrudRepository;
  3. import java.util.UUID;
  4. /**
  5. * Created by kevin on 2018/9/13.
  6. */
  7. public interface UserRepository extends CrudRepository<User,UUID>{
  8. }

到此为止,已经有比较完备的用户管理核心部分了。有了这么多代码,先创建个测试,检测代码的正确度。

UserRepositoryTest.java

  1. package com.babyrabbit.demo.user;
  2. import org.junit.Test;
  3. import org.junit.runner.RunWith;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
  6. import org.springframework.test.context.junit4.SpringRunner;
  7. import java.util.HashSet;
  8. import java.util.UUID;
  9. import static org.assertj.core.api.Assertions.assertThat;
  10. @RunWith(SpringRunner.class)
  11. @DataJpaTest
  12. public class UserRepositoryTest {
  13. @Autowired
  14. private UserRepository repository;
  15. @Test
  16. public void testStoreUser() {
  17. HashSet<UserRole> roles = new HashSet<>();
  18. roles.add(UserRole.OFFICER);
  19. User user = repository.save(new User(UUID.randomUUID(),
  20. "alex.foley@beverly-hills.com",
  21. "my-secret-pwd",
  22. roles));
  23. assertThat(user).isNotNull();
  24. assertThat(repository.count()).isEqualTo(1L);
  25. }
  26. }

上面的测试代码中,

  • @DataJpaTest告诉测试系统,这是个与jpa有关的测试
  • 使用注入的方式将UserRepository注入到了这个测试中

重构User

上面的User亦可以完成用户信息的存储,但是有三个可以改进的地方:

  1. 使用专门的类来表示主键
  2. 为所有实体类抽象一个父类,使用统一方式来定义主键
  3. 将生成主键的逻辑集中放在仓库的一个地方

在上面的代码中,使用UUID作为实体类的主键,但是如果使用专门的类而不是long或者UUID来做主键,有一下好处:

  1. 能更清楚的表达含义,例如,使用UserId类作为User的主键,这样比UUID或者long更能表达含义。
  2. 对于特定的实体使用特定类型的主键,可以防止主键混用,也就是不能把Uer的主键UserId赋值给Department。
  3. 当可能会修改主键的类型时,使用专门的主键类可以减少底层键值类型变化所带来的影响。

下面就为主键和实体创建父类。

首先定义主键类接口EntityId:

EntityId.java

  1. package com.babyrabbit.demo.orm.jpa;
  2. import java.io.Serializable;
  3. /**
  4. * Interface for primary keys of entities.
  5. *
  6. * @param <T> the underlying type of the entity id
  7. */
  8. public interface EntityId<T> extends Serializable {
  9. T getId();
  10. String asString();
  11. }

定义主键父类AbstractEntityId,实现主键接口EntityId:

AbstractEntityId.java

  1. package com.babyrabbit.demo.orm.jpa;
  2. import com.babyrabbit.demo.util.ArtifactForFramework;
  3. import lombok.Data;
  4. import javax.persistence.MappedSuperclass;
  5. import java.io.Serializable;
  6. import java.util.Objects;
  7. import static com.google.common.base.MoreObjects.toStringHelper;
  8. @Data
  9. @MappedSuperclass
  10. public abstract class AbstractEntityId<T extends Serializable>
  11. implements Serializable,EntityId<T> {
  12. private T id;
  13. @ArtifactForFramework
  14. protected AbstractEntityId() {
  15. }
  16. protected AbstractEntityId(T id) {
  17. this.id = Objects.requireNonNull(id);
  18. }
  19. @Override
  20. public boolean equals(Object o) {
  21. boolean result = false;
  22. if (this == o) {
  23. result = true;
  24. } else if (o instanceof AbstractEntityId) {
  25. AbstractEntityId other = (AbstractEntityId) o;
  26. result = Objects.equals(id, other.id);
  27. }
  28. return result;
  29. }
  30. @Override
  31. public int hashCode() {
  32. return Objects.hash(id);
  33. }
  34. @Override
  35. public String toString() {
  36. return toStringHelper(this)
  37. .add("id", id)
  38. .toString();
  39. }
  40. }

上面的代码中,@MappedSuperclass是jpa提供的一个注解,通过这个注解,可以将该实体类当成基类实体,它不会隐射到数据库表,但继承它的子类实体在隐射时会自动扫描该基类实体的隐射属性,添加到子类实体的对应数据库表中。

这个注解使用规则如下:

  1. 使用在父类上面,是用来标识父类的
  2. 使用这个注解标识的类表示其不能映射到数据库表,因为其不是一个完整的实体类,但是它所拥有的属性能够隐射在其子类对用的数据库表中
  3. 使用这个注解标识得类不能再有@Entity或@Table注解

下面,为实体类定义公共接口Entity:

Entity.java

  1. package com.babyrabbit.demo.orm.jpa;
  2. /**
  3. * Interface for entity objects.
  4. *
  5. * @param <T> the type of {@link EntityId} that will be used in this entity
  6. */
  7. public interface Entity<T extends EntityId> {
  8. T getId();
  9. }

为实体类定义公共父类AbstractEntity,实现Entity接口:

AbstractEntity.java

  1. package com.babyrabbit.demo.orm.jpa;
  2. import com.babyrabbit.demo.util.ArtifactForFramework;
  3. import lombok.Getter;
  4. import javax.persistence.EmbeddedId;
  5. import javax.persistence.MappedSuperclass;
  6. import java.util.Objects;
  7. import static com.google.common.base.MoreObjects.toStringHelper;
  8. /**
  9. * Abstract super class for entities. We are assuming that early primary key
  10. * generation will be used.
  11. *
  12. * @param <T> the type of {@link EntityId} that will be used for this entity
  13. */
  14. @Getter
  15. @MappedSuperclass
  16. public abstract class AbstractEntity<T extends EntityId> implements Entity<T> {
  17. @EmbeddedId
  18. private T id;
  19. @ArtifactForFramework
  20. protected AbstractEntity() {
  21. }
  22. @Override
  23. public boolean equals(Object obj) {
  24. boolean result = false;
  25. if (this == obj) {
  26. result = true;
  27. } else if (obj instanceof AbstractEntity) {
  28. AbstractEntity other = (AbstractEntity) obj;
  29. result = Objects.equals(id, other.id);
  30. }
  31. return result;
  32. }
  33. @Override
  34. public int hashCode() {
  35. return Objects.hash(id);
  36. }
  37. @Override
  38. public String toString() {
  39. return toStringHelper(this)
  40. .add("id", id)
  41. .toString();
  42. }
  43. }

上面代码中,@EmbeddedId是jpa的注解,这个注解告诉jpa,这个主键类是不需要单独生成数据表存储的。

有个自定义注解@ArtifactForFramework,这个注解只是起标记说明的作用,标记这个只是因为框架需要这样一个构造方法,在代码中没有什么使用场景。

ArtifactForFramework.java

  1. package com.babyrabbit.demo.util;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. @Retention(value = RetentionPolicy.SOURCE)
  5. public @interface ArtifactForFramework {
  6. }

创建和实体和主键的父类后,就可以重构User类。 创建UserId类:

UserId.java

  1. import com.babyrabbit.demo.orm.jpa.AbstractEntityId;
  2. import java.util.UUID;
  3. public class UserId extends AbstractEntityId<UUID> {
  4. protected UserId(){
  5. }
  6. public UserId(UUID id){
  7. super(id);
  8. }
  9. @Override
  10. public String asString() {
  11. return super.getId().toString();
  12. }
  13. }

重构后的User类如下:

User.java

  1. package com.babyrabbit.demo.user;
  2. import com.babyrabbit.demo.orm.jpa.AbstractEntity;
  3. import lombok.Getter;
  4. import javax.persistence.*;
  5. import javax.validation.constraints.NotNull;
  6. import java.util.HashSet;
  7. import java.util.Set;
  8. /**
  9. * Created by kevin on 2018/9/13.
  10. */
  11. @Getter
  12. @Entity
  13. @Table(name="tb_user")
  14. public class User extends AbstractEntity<UserId>{
  15. private String email;
  16. private String password;
  17. @ElementCollection(fetch = FetchType.EAGER)
  18. @Enumerated(EnumType.STRING)
  19. @NotNull
  20. private Set<UserRole> roles;
  21. protected User(){}
  22. public User(UserId id, String email, String password, Set<UserRole> roles) {
  23. super(id);
  24. this.email = email;
  25. this.password = password;
  26. this.roles = roles;
  27. }
  28. public static User createOfficer(UserId userId, String officerEmail, String encode) {
  29. HashSet<UserRole> roles = new HashSet<>();
  30. roles.add(UserRole.OFFICER);
  31. return new User(userId,officerEmail,encode,roles);
  32. }
  33. public static User createCaptain(UserId userId, String captainEmail, String encode) {
  34. HashSet<UserRole> roles = new HashSet<>();
  35. roles.add(UserRole.CAPTAIN);
  36. return new User(userId,captainEmail,encode,roles);
  37. }
  38. }

自定义主键生成逻辑

主键生成逻辑需要放在仓库中,因此需要在UserRepository中提供nextId()方法来生成UserId实例。由于UserRepository是接口,所以不能直接在UserRepository中提供nextId()的实现,因此需要做点额外工作,在一个额外实现类中实现这个功能.

自定义UserRepositoryCustom接口:

UserRepositoryCustom.java

  1. package com.babyrabbit.demo.user;
  2. /**
  3. * Created by kevin on 2018/9/13.
  4. */
  5. public interface UserRepositoryCustom {
  6. UserId nextId();
  7. }

然后将UserRepositoryCustom添加到UserRepository接口中:

UserRepository.java

  1. package com.babyrabbit.demo.user;
  2. import org.springframework.data.repository.CrudRepository;
  3. import java.util.UUID;
  4. /**
  5. * Created by kevin on 2018/9/13.
  6. */
  7. public interface UserRepository extends CrudRepository<User,UUID>, UserRepositoryCustom{
  8. }

下面为UserRepositoryCustom接口创建实现类UserRepositoryImpl,实现nextId()的逻辑:

UserRepositoryImpl.java

  1. package com.babyrabbit.demo.user;
  2. import com.babyrabbit.demo.orm.jpa.UniqueIdGenerator;
  3. import java.util.UUID;
  4. public class UserRepositoryImpl implements UserRepositoryCustom {
  5. private final UniqueIdGenerator<UUID> generator;
  6. public UserRepositoryImpl(UniqueIdGenerator<UUID> generator) {
  7. this.generator = generator;
  8. }
  9. @Override
  10. public UserId nextId() {
  11. return new UserId(generator.getNextUniqueId());
  12. }
  13. }

当程序运行时,spring data会将UserRepositoryImpl的代码与Spring data的CrudRepository的代码合并,在UserRepository中会看到UserRepositoryCustom和CrudRepository里的方法。

UserRepositoryImpl中注入了UniqueIdGenerator,因此需要创建UniqueIdGenerator,并注册为spring bean.

UniqueIdGenerator.java

  1. package com.babyrabbit.demo.orm.jpa;
  2. /**
  3. * Created by kevin on 2018/9/13.
  4. */
  5. public interface UniqueIdGenerator<T> {
  6. T getNextUniqueId();
  7. }

InMemoryUniqueIdGenerator.java

  1. package com.babyrabbit.demo.orm.jpa;
  2. import java.util.UUID;
  3. /**
  4. * Created by kevin on 2018/9/13.
  5. */
  6. public class InMemoryUniqueIdGenerator implements UniqueIdGenerator<UUID>{
  7. @Override
  8. public UUID getNextUniqueId() {
  9. return UUID.randomUUID();
  10. }
  11. }

在BabyRabbitConfiguration中,将UniqueIdGenerator注册到spring中:

BabyRabbitConfiguration.java

  1. package com.babyrabbit.demo.conf;
  2. import com.babyrabbit.demo.orm.jpa.InMemoryUniqueIdGenerator;
  3. import com.babyrabbit.demo.orm.jpa.UniqueIdGenerator;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import java.util.UUID;
  7. /**
  8. * Created by kevin on 2018/9/13.
  9. */
  10. @Configuration
  11. public class BabyRabbitConfiguration {
  12. @Bean
  13. public UniqueIdGenerator<UUID> uniqueIdGenerator() {
  14. return new InMemoryUniqueIdGenerator();
  15. }
  16. }

重构了ID的定义后,UserRepositoryTest也需要做点调整才能通过测试:

UserRepositoryTest.java

  1. package com.babyrabbit.demo.user;
  2. import com.babyrabbit.demo.orm.jpa.InMemoryUniqueIdGenerator;
  3. import com.babyrabbit.demo.orm.jpa.UniqueIdGenerator;
  4. import org.junit.Test;
  5. import org.junit.runner.RunWith;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
  8. import org.springframework.boot.test.context.TestConfiguration;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.test.context.junit4.SpringRunner;
  11. import java.util.HashSet;
  12. import java.util.UUID;
  13. import static org.assertj.core.api.Assertions.assertThat;
  14. @RunWith(SpringRunner.class)
  15. @DataJpaTest
  16. public class UserRepositoryTest {
  17. @Autowired
  18. private UserRepository repository;
  19. @Test
  20. public void testStoreUser() {
  21. HashSet<UserRole> roles = new HashSet<>();
  22. roles.add(UserRole.OFFICER);
  23. User user = repository.save(new User(repository.nextId(),
  24. "alex.foley@beverly-hills.com",
  25. "my-secret-pwd",
  26. roles));
  27. assertThat(user).isNotNull();
  28. assertThat(repository.count()).isEqualTo(1L);
  29. }
  30. @TestConfiguration
  31. static class TestConfig {
  32. @Bean
  33. public UniqueIdGenerator<UUID> generator() {
  34. return new InMemoryUniqueIdGenerator();
  35. }
  36. }
  37. }

REST API 的安全

Spring Security提供了全套的安全解决方案。对于移动应用,将使用Oauth2安全方案。Spring Security Oauth2提供了Oauth2实现。

注意: Oauth2需要使用HTTPS协议。

使用Oauth2安全方案后,可以通过client secret和client id 来验证client应用,使用username, password来验证用户。相应的,验证过的用户可以获取access token来访问API。

为了使用spring security oauth2,需要使用AutoConfigure来简化oauth2的配置,这需要在pom.xml中添加依赖:

  1. <dependency>
  2. <groupId>org.springframework.security.oauth.boot</groupId>
  3. <artifactId>spring-security-oauth2-autoconfigure</artifactId>
  4. <version>2.0.1.RELEASE</version>
  5. </dependency>

Oauth协议中,提供了两部分组件:

  • 授权服务:负责给客户端和用户授权
  • 资源服务:负责定义认证用户和未认证用户可以访问应用的哪些部分

创建OAuth2ServerConfiguration来配置授权和资源服务:

OAuth2ServerConfiguration.java

  1. package com.babyrabbit.demo.conf;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.beans.factory.annotation.Value;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.http.HttpMethod;
  6. import org.springframework.security.authentication.AuthenticationManager;
  7. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  8. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  9. import org.springframework.security.core.userdetails.UserDetailsService;
  10. import org.springframework.security.crypto.password.PasswordEncoder;
  11. import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
  12. import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
  13. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
  14. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
  15. import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
  16. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
  17. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
  18. import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
  19. import org.springframework.security.oauth2.provider.token.TokenStore;
  20. /**
  21. * Created by kevin on 2018/9/14.
  22. */
  23. public class OAuth2ServerConfiguration {
  24. @Value("${resource_id}")
  25. private String resourceId;
  26. @Configuration
  27. @EnableAuthorizationServer
  28. protected class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
  29. @Autowired
  30. private AuthenticationManager authenticationManager;
  31. @Autowired
  32. private UserDetailsService userDetailsService;
  33. @Autowired
  34. private PasswordEncoder passwordEncoder;
  35. @Autowired
  36. private TokenStore tokenStore;
  37. @Override
  38. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  39. security.passwordEncoder(passwordEncoder);
  40. }
  41. @Override
  42. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  43. clients.inMemory()
  44. .withClient("baby-mobile-client")
  45. .authorizedGrantTypes("password", "refresh_token")
  46. .scopes("mobile_app")
  47. .resourceIds(resourceId)
  48. .secret(passwordEncoder.encode("ccUyb6vS4S8nxfbKPCrN"));
  49. }
  50. @Override
  51. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  52. endpoints.tokenStore(tokenStore)
  53. .authenticationManager(authenticationManager)
  54. .userDetailsService(userDetailsService);
  55. }
  56. }
  57. @Configuration
  58. @EnableResourceServer
  59. @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
  60. protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
  61. @Override
  62. public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
  63. resources.resourceId(resourceId);
  64. }
  65. @Override
  66. public void configure(HttpSecurity http) throws Exception {
  67. http.authorizeRequests()
  68. .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll()
  69. .and()
  70. .antMatcher("/api/**").authorizeRequests()
  71. .anyRequest().authenticated();
  72. }
  73. }
  74. }

上面AuthorizationServerConfiguration代码中:

  • 只对ID是baby-mobile-client的客户端做认证
  • 支持password和refresh_token两种认证模式
  • scopes定义了可以通过access token访问的程序部分

上面ResourceServerConfiguration代码中:

  • 允许所有对于 '/api/**' 的Options操作
  • 其他访问 '/api/**' 的操作,需要授权
  • 所有对 '/api/**' 的非OPTIONS访问都需要认证

上面的代码中需要注入PasswordEncoder和 TokenStore, 因此,需要在Spring中注册这两个bean:

  1. package com.babyrabbit.demo.conf;
  2. import com.babyrabbit.demo.orm.jpa.InMemoryUniqueIdGenerator;
  3. import com.babyrabbit.demo.orm.jpa.UniqueIdGenerator;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  7. import org.springframework.security.crypto.password.PasswordEncoder;
  8. import org.springframework.security.oauth2.provider.token.TokenStore;
  9. import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
  10. import java.util.UUID;
  11. /**
  12. * Created by kevin on 2018/9/13.
  13. */
  14. @Configuration
  15. public class BabyRabbitConfiguration {
  16. @Bean
  17. public UniqueIdGenerator<UUID> uniqueIdGenerator() {
  18. return new InMemoryUniqueIdGenerator();
  19. }
  20. @Bean
  21. public PasswordEncoder passwordEncoder() {
  22. return new BCryptPasswordEncoder();
  23. }
  24. @Bean
  25. public TokenStore tokenStore() {
  26. return new InMemoryTokenStore();
  27. }
  28. }

UserDetailsService

为了实现认证功能,需要实现UserDetailsService。这个接口只有一个方法 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

UserDetails

这里,先要构建一个UserDetails实现。

ApplicationUserDetails.java

  1. package com.babyrabbit.demo.security;
  2. import com.babyrabbit.demo.user.User;
  3. import com.babyrabbit.demo.user.UserId;
  4. import com.babyrabbit.demo.user.UserRole;
  5. import org.springframework.security.core.authority.SimpleGrantedAuthority;
  6. import java.util.Collection;
  7. import java.util.Set;
  8. import java.util.stream.Collectors;
  9. public class ApplicationUserDetails extends org.springframework.security.core.userdetails.User {
  10. private static final String ROLE_PREFIX = "ROLE_";
  11. private final UserId userId;
  12. public ApplicationUserDetails(User user) {
  13. super(user.getEmail(), user.getPassword(), createAuthorities(user.getRoles()));
  14. this.userId = user.getId();
  15. }
  16. public UserId getUserId() {
  17. return userId;
  18. }
  19. private static Collection<SimpleGrantedAuthority> createAuthorities(Set<UserRole> roles) {
  20. return roles.stream()
  21. .map(userRole -> new SimpleGrantedAuthority(ROLE_PREFIX + userRole.name()))
  22. .collect(Collectors.toSet());
  23. }
  24. }

自定义的UserDetails继承了 org.springframework.security.core.userdetails.User.

当然,也可以直接在User对象里实现UserDetails:

public class User extends AbstractEntity<UserId> implements UserDetails {

如果要这么做,需要注意三点:

  1. getAuthorities()方法返回这个用户的授权列表。可以参照org.springframework.security.core.userdetails.User 里的实现
  2. User需要实现序列化
  3. Spring Security 会缓存UserDetails,因此在控制器中,使用@AuthenticationPrincipal User user 注入已认证用户对象

UserRepository

要实现能读取用户信息的能力,需要在用户仓库里增加一个方法:

UserRepository.java

  1. package com.babyrabbit.demo.user;
  2. import org.springframework.data.repository.CrudRepository;
  3. import java.util.Optional;
  4. import java.util.UUID;
  5. /**
  6. * Created by kevin on 2018/9/13.
  7. */
  8. public interface UserRepository extends CrudRepository<User,UUID>, UserRepositoryCustom{
  9. Optional<User> findByEmailIgnoreCase(String email);
  10. }

这里用了Optional作为返回类型,无论findByEmailIgnoreCase方法是否找到user的信息,都不会直接返回null给调用者,这样防止了空指针例外。

相应的,修改UserRepositoryTest.java,测试这个新方法:

UserRepositoryTest.java

  1. package com.babyrabbit.demo.user;
  2. import com.babyrabbit.demo.orm.jpa.InMemoryUniqueIdGenerator;
  3. import com.babyrabbit.demo.orm.jpa.UniqueIdGenerator;
  4. import org.junit.Test;
  5. import org.junit.runner.RunWith;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
  8. import org.springframework.boot.test.context.TestConfiguration;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.test.context.junit4.SpringRunner;
  11. import java.util.HashSet;
  12. import java.util.Locale;
  13. import java.util.Optional;
  14. import java.util.UUID;
  15. import static org.assertj.core.api.Assertions.assertThat;
  16. @RunWith(SpringRunner.class)
  17. @DataJpaTest
  18. public class UserRepositoryTest {
  19. @Autowired
  20. private UserRepository repository;
  21. @Test
  22. public void testStoreUser() {
  23. HashSet<UserRole> roles = new HashSet<>();
  24. roles.add(UserRole.OFFICER);
  25. User user = repository.save(new User(repository.nextId(),
  26. "alex.foley@beverly-hills.com",
  27. "my-secret-pwd",
  28. roles));
  29. assertThat(user).isNotNull();
  30. assertThat(repository.count()).isEqualTo(1L);
  31. }
  32. @Test
  33. public void testFindByEmail() {
  34. User user = Users.newRandomOfficer();
  35. repository.save(user);
  36. Optional<User> optional = repository.findByEmailIgnoreCase(user.getEmail());
  37. assertThat(optional).isNotEmpty()
  38. .contains(user);
  39. }
  40. @Test
  41. public void testFindByEmailIgnoringCase() {
  42. User user = Users.newRandomOfficer();
  43. repository.save(user);
  44. Optional<User> optional = repository.findByEmailIgnoreCase(user.getEmail()
  45. .toUpperCase
  46. (Locale.US));
  47. assertThat(optional).isNotEmpty()
  48. .contains(user);
  49. }
  50. @Test
  51. public void testFindByEmail_unknownEmail() {
  52. User user = Users.newRandomOfficer();
  53. repository.save(user);
  54. Optional<User> optional = repository.findByEmailIgnoreCase("will.not@find.me");
  55. assertThat(optional).isEmpty();
  56. }
  57. @TestConfiguration
  58. static class TestConfig {
  59. @Bean
  60. public UniqueIdGenerator<UUID> generator() {
  61. return new InMemoryUniqueIdGenerator();
  62. }
  63. }
  64. }

上面的测试代码中,新增了三个测试: testFindByEmail, testFindByEmailIgnoringCase, testFindByEmail_unknownEmail . 在新增的测试中,使用了Users这个类,这是一个工厂类,用来构造User。 我喜欢为每个实体对象都准备一个工厂类,这样就可以把构造实体对象的逻辑集中化。

ApplicationUserDetailsService

下面创建ApplicationUserDetailsService.java, 实现UserDetailsService, 提供加载user信息的服务。

ApplicationUserDetailsService.java

  1. package com.babyrabbit.demo.security;
  2. import com.babyrabbit.demo.user.User;
  3. import com.babyrabbit.demo.user.UserRepository;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.security.core.userdetails.UserDetails;
  6. import org.springframework.security.core.userdetails.UserDetailsService;
  7. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  8. import org.springframework.stereotype.Service;
  9. import static java.lang.String.format;
  10. @Service
  11. public class ApplicationUserDetailsService implements UserDetailsService {
  12. private final UserRepository userRepository;
  13. @Autowired
  14. public ApplicationUserDetailsService(UserRepository userRepository) {
  15. this.userRepository = userRepository;
  16. }
  17. @Override
  18. public UserDetails loadUserByUsername(String username) {
  19. User user = userRepository.findByEmailIgnoreCase(username)
  20. .orElseThrow(() -> new UsernameNotFoundException(
  21. String.format("User with email %s could not be found",
  22. username)));
  23. return new ApplicationUserDetails(user);
  24. }
  25. }

由于User仓库的findByEmailIgnoreCase 方法返回的是Optional类型的返回值,因此当Optional中没有期望的值时,可以使用orElseThrow 来抛出例外终止代码执行流程。

已经创建了ApplicationUserDetailsService,下面就可以为其创建测试了。

ApplicationUserDetailsServiceTest.java

  1. package com.babyrabbit.demo.security;
  2. import com.babyrabbit.demo.user.UserRepository;
  3. import com.babyrabbit.demo.user.Users;
  4. import org.junit.Test;
  5. import org.springframework.security.core.GrantedAuthority;
  6. import org.springframework.security.core.userdetails.UserDetails;
  7. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  8. import java.util.Optional;
  9. import static org.assertj.core.api.Assertions.assertThat;
  10. import static org.mockito.Matchers.anyString;
  11. import static org.mockito.Mockito.mock;
  12. import static org.mockito.Mockito.when;
  13. public class ApplicationUserDetailsServiceTest {
  14. @Test
  15. public void givenExistingUsername_whenLoadingUser_userIsReturned() {
  16. UserRepository repository = mock(UserRepository.class);
  17. ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository);
  18. when(repository.findByEmailIgnoreCase(Users.OFFICER_EMAIL))
  19. .thenReturn(Optional
  20. .of(Users.officer()));
  21. UserDetails userDetails = service.loadUserByUsername(Users.OFFICER_EMAIL);
  22. assertThat(userDetails).isNotNull();
  23. assertThat(userDetails.getUsername()).isEqualTo(Users.OFFICER_EMAIL);
  24. assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority).contains("ROLE_OFFICER");
  25. assertThat(userDetails).isInstanceOfSatisfying(ApplicationUserDetails.class,
  26. applicationUserDetails -> {
  27. assertThat(applicationUserDetails.getUserId()).isEqualTo(Users.officer().getId());
  28. });
  29. }
  30. @Test(expected = UsernameNotFoundException.class)
  31. public void givenNotExistingUsername_whenLoadingUser_exceptionThrown() {
  32. UserRepository repository = mock(UserRepository.class);
  33. ApplicationUserDetailsService service = new ApplicationUserDetailsService(repository);
  34. when(repository.findByEmailIgnoreCase(anyString())).thenReturn(Optional.empty());
  35. service.loadUserByUsername("i@donotexist.com");
  36. }
  37. }

测试获取access token

前面已经把Spring Security OAuth2集成到工程里了,下面就可以尝试获取access token. 在执行测试前,需要先在库里创建几个User。

DevelopmentDbInitializer

DevelopmentDbInitializer类实现了ApplicationRunner接口,spring boot在启动时,会执行所有这个接口的实现类。

DevelopmentDbInitializer.java

  1. package com.babyrabbit.demo;
  2. import com.babyrabbit.demo.user.UserService;
  3. import com.babyrabbit.demo.util.SpringProfiles;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.boot.ApplicationArguments;
  6. import org.springframework.boot.ApplicationRunner;
  7. import org.springframework.context.annotation.Profile;
  8. import org.springframework.stereotype.Component;
  9. @Component
  10. @Profile(SpringProfiles.DEV)
  11. public class DevelopmentDbInitializer implements ApplicationRunner {
  12. private final UserService userService;
  13. @Autowired
  14. public DevelopmentDbInitializer(UserService userService) {
  15. this.userService = userService;
  16. }
  17. @Override
  18. public void run(ApplicationArguments applicationArguments) {
  19. createTestUsers();
  20. }
  21. private void createTestUsers() {
  22. userService.createOfficer("officer@example.com", "officer");
  23. }
  24. }

UserService

UserService完成与User有关业务逻辑的服务接口。

UserService.java

  1. package com.babyrabbit.demo.user;
  2. public interface UserService {
  3. User createOfficer(String email, String password);
  4. }

UserServiceImpl.java

  1. package com.babyrabbit.demo.user;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.security.crypto.password.PasswordEncoder;
  4. import org.springframework.stereotype.Service;
  5. @Service
  6. public class UserServiceImpl implements UserService {
  7. private final UserRepository repository;
  8. private final PasswordEncoder passwordEncoder;
  9. @Autowired
  10. public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) {
  11. this.repository = repository;
  12. this.passwordEncoder = passwordEncoder;
  13. }
  14. @Override
  15. public User createOfficer(String email, String password) {
  16. User user = User.createOfficer(repository.nextId(), email,
  17. passwordEncoder.encode(password));
  18. return repository.save(user);
  19. }
  20. }

下面使用maven 启动spring boot 服务: mvn spring-boot:run --spring.profiles.active=dev

使用POSTMAN工具来测试服务:

  • 选择POST请求
  • 地址为: http://localhost:8080/oauth/token
  • 在Basic Auth选项页中,username: baby-mobile-client, password: ccUyb6vS4S8nxfbKPCrN
  • Priview Request, 在Body选项页,选择x-www-form-urlencoded,填入下面的值:
keyvalue
usernameofficer@example.com
passwordofficer
grant_typepassword
client_idbaby-mobile-client
client_secretccUyb6vS4S8nxfbKPCrN

测试成,服务返回数据:

  1. {
  2. "access_token": "4329339a-ea9b-4781-86d1-1f49193fa5b3",
  3. "token_type": "bearer",
  4. "refresh_token": "90d46c41-9aa0-4c23-88e3-8d4ca4e77b30",
  5. "expires_in": 43199,
  6. "scope": "mobile_app"
  7. }

在返回结果中,包含了access_token和refresh_token。access_token是我们在访问服务的时候需要用的。当access_token过期后,可以使用refresh_token获取一个新的access_token,而不需要重新提供用户名密码进行验证。 当然,refresh_token也会过期,这时候就需要用户重新认证,获取新的access_token和refresh_token。

为了能自动测试 /oauth/token 接口,需要准备点单元测试,这样在每次修改代码后,都会执行单元测试验证接口的正确性。

OAuth2ServerConfigurationTest.java

  1. package com.babyrabbit.demo.security;
  2. import com.babyrabbit.demo.user.UserService;
  3. import com.babyrabbit.demo.user.Users;
  4. import com.babyrabbit.demo.util.SpringProfiles;
  5. import org.junit.Test;
  6. import org.junit.runner.RunWith;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
  9. import org.springframework.boot.test.context.SpringBootTest;
  10. import org.springframework.test.context.ActiveProfiles;
  11. import org.springframework.test.context.junit4.SpringRunner;
  12. import org.springframework.test.web.servlet.MockMvc;
  13. import org.springframework.util.LinkedMultiValueMap;
  14. import org.springframework.util.MultiValueMap;
  15. import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
  16. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
  17. import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
  18. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  19. @RunWith(SpringRunner.class)
  20. @SpringBootTest
  21. @AutoConfigureMockMvc
  22. @ActiveProfiles(SpringProfiles.TEST)
  23. public class OAuth2ServerConfigurationTest {
  24. @Autowired
  25. private MockMvc mvc;
  26. @Autowired
  27. private UserService userService;
  28. @Test
  29. public void testGetAccessTokenAsOfficer() throws Exception {
  30. userService.createOfficer(Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD);
  31. String clientId = "baby-mobile-client";
  32. String clientSecret = "ccUyb6vS4S8nxfbKPCrN";
  33. MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
  34. params.add("grant_type", "password");
  35. params.add("client_id", clientId);
  36. params.add("client_secret", clientSecret);
  37. params.add("username", Users.OFFICER_EMAIL);
  38. params.add("password", Users.OFFICER_PASSWORD);
  39. mvc.perform(post("/oauth/token")
  40. .params(params)
  41. .with(httpBasic(clientId, clientSecret))
  42. .accept("application/json;charset=UTF-8"))
  43. .andExpect(status().isOk())
  44. .andExpect(content().contentType("application/json;charset=UTF-8"))
  45. .andDo(print())
  46. .andExpect(jsonPath("access_token").isString())
  47. .andExpect(jsonPath("token_type").value("bearer"))
  48. .andExpect(jsonPath("refresh_token").isString())
  49. .andExpect(jsonPath("expires_in").isNumber())
  50. .andExpect(jsonPath("scope").value("mobile_app"));
  51. }
  52. }

上面的代码中:

  • @SpringBootTest 要求测试需要一个mock servlet 环境
  • @AutoConfigureMockMvc 标记这个mock servlet环境会被自动配置,不需要为了这个测试做特殊配置
  • 自动注入MockMvc, 可以用来测试API接口

把自定义的配置分离出来

在这个工程中,有一些自定义的属性,例如,client_id, client_secret等,这些配置是与安全相关的,这些信息最好放在配置文件了,而不是硬编码在代码中。spring boot对这样的需求提供了支持。

首先,需要在工程pom.xml中增减依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-configuration-processor</artifactId>
  4. <optional>true</optional>
  5. </dependency>

然后,创建一个POJO来存储这两个属性:

SecurityConfiguration.java

  1. package com.babyrabbit.demo.conf;
  2. import lombok.Data;
  3. import org.springframework.boot.context.properties.ConfigurationProperties;
  4. import org.springframework.stereotype.Component;
  5. @Data
  6. @Component
  7. @ConfigurationProperties(prefix = "babyrabbit-security")
  8. public class SecurityConfiguration {
  9. private String mobileAppClientId;
  10. private String mobileAppClientSecret;
  11. }

@ConfigurationProperties注解告诉配置处理器这个类的属性字段对于外部配置是可见的。

有了这个配置后,就可以修改AuthorizationServerConfiguration, 自动注入SecurityConfiguration了:

OAuth2ServerConfiguration.java

  1. package com.babyrabbit.demo.conf;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.beans.factory.annotation.Value;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.http.HttpMethod;
  6. import org.springframework.security.authentication.AuthenticationManager;
  7. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  8. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  9. import org.springframework.security.core.userdetails.UserDetailsService;
  10. import org.springframework.security.crypto.password.PasswordEncoder;
  11. import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
  12. import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
  13. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
  14. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
  15. import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
  16. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
  17. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
  18. import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
  19. import org.springframework.security.oauth2.provider.token.TokenStore;
  20. /**
  21. * Created by kevin on 2018/9/14.
  22. */
  23. @Configuration
  24. public class OAuth2ServerConfiguration {
  25. @Value("${resource_id}")
  26. private String resourceId;
  27. @Configuration
  28. @EnableAuthorizationServer
  29. protected class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
  30. @Autowired
  31. private AuthenticationManager authenticationManager;
  32. @Autowired
  33. private UserDetailsService userDetailsService;
  34. @Autowired
  35. private PasswordEncoder passwordEncoder;
  36. @Autowired
  37. private TokenStore tokenStore;
  38. @Autowired
  39. private SecurityConfiguration securityConfiguration;
  40. @Override
  41. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  42. security.passwordEncoder(passwordEncoder);
  43. }
  44. @Override
  45. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  46. clients.inMemory()
  47. .withClient(securityConfiguration.getMobileAppClientId())
  48. .authorizedGrantTypes("password", "refresh_token")
  49. .scopes("mobile_app")
  50. .resourceIds(resourceId)
  51. .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret()));
  52. }
  53. @Override
  54. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  55. endpoints.tokenStore(tokenStore)
  56. .authenticationManager(authenticationManager)
  57. .userDetailsService(userDetailsService);
  58. }
  59. }
  60. @Configuration
  61. @EnableResourceServer
  62. @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
  63. protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
  64. @Override
  65. public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
  66. resources.resourceId(resourceId);
  67. }
  68. @Override
  69. public void configure(HttpSecurity http) throws Exception {
  70. http.authorizeRequests()
  71. .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll()
  72. .and()
  73. .antMatcher("/api/**").authorizeRequests()
  74. .anyRequest().authenticated();
  75. }
  76. }
  77. }

之后,就可以在application-dev.properties,application-staging.properties,application-prod.properties,application-test.properties中配置相应的值:

例如,在application-dev.properties中的配置如下:

  1. baby-security.mobile-app-client-id=baby-mobile-client
  2. baby-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN

在application-test.properties中配置如下:

  1. baby-security.mobile-app-client-id=test-client-id
  2. baby-security.mobile-app-client-secret=test-client-secret

REST 控制器

要实现API业务服务,需要通过REST提供API 接口。

GET 接口

首先提供一个查看用户自己信息的接口,这个接口是一个GET接口。

UserRestController.java

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.security.ApplicationUserDetails;
  3. import com.babyrabbit.demo.user.User;
  4. import com.babyrabbit.demo.user.UserNotFoundException;
  5. import com.babyrabbit.demo.user.UserService;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.security.core.annotation.AuthenticationPrincipal;
  8. import org.springframework.web.bind.annotation.GetMapping;
  9. import org.springframework.web.bind.annotation.RequestMapping;
  10. import org.springframework.web.bind.annotation.RestController;
  11. @RestController
  12. @RequestMapping("/api/users")
  13. public class UserRestController {
  14. private final UserService service;
  15. @Autowired
  16. public UserRestController(UserService service) {
  17. this.service = service;
  18. }
  19. @GetMapping("/me")
  20. public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) {
  21. User user = service.getUser(userDetails.getUserId())
  22. .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId()));
  23. return UserDto.fromUser(user);
  24. }
  25. }

客户端可以通过 get /api/users/me 来查看登录用户的信息。

UserNotFoundException.java

  1. package com.babyrabbit.demo.user;
  2. import org.springframework.http.HttpStatus;
  3. import org.springframework.web.bind.annotation.ResponseStatus;
  4. @ResponseStatus(HttpStatus.NOT_FOUND)
  5. public class UserNotFoundException extends RuntimeException {
  6. public UserNotFoundException(UserId userId) {
  7. super(String.format("Could not find user with id %s", userId.asString()));
  8. }
  9. }

这个自定义例外用来在未找到用户时,打断程序的执行流程。使用@ResponseStatus注解,当这个例外从controller抛出时,spring框架会捕捉并处理,生成默认的响应,这时候,这个响应使用的状态码就是这个注解里的值。

UserDto.java

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.user.User;
  3. import com.babyrabbit.demo.user.UserId;
  4. import com.babyrabbit.demo.user.UserRole;
  5. import lombok.Value;
  6. import java.util.Set;
  7. @Value
  8. public class UserDto {
  9. private final UserId id;
  10. private final String email;
  11. private final Set<UserRole> roles;
  12. public static UserDto fromUser(User user) {
  13. return new UserDto(user.getId(),
  14. user.getEmail(),
  15. user.getRoles());
  16. }
  17. }

不能直接使用实体对象作为返回数据的载体,使用专用的传输对象。

UserRestController的测试

为这个接口创建测试用例。

UserRestControllerTest.java

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.conf.OAuth2ServerConfiguration;
  3. import com.babyrabbit.demo.conf.SecurityConfiguration;
  4. import com.babyrabbit.demo.user.UserService;
  5. import com.babyrabbit.demo.user.Users;
  6. import com.babyrabbit.demo.util.SpringProfiles;
  7. import org.junit.Test;
  8. import org.junit.runner.RunWith;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
  11. import org.springframework.boot.test.context.TestConfiguration;
  12. import org.springframework.boot.test.mock.mockito.MockBean;
  13. import org.springframework.context.annotation.Bean;
  14. import org.springframework.context.annotation.Import;
  15. import org.springframework.security.core.userdetails.UserDetailsService;
  16. import org.springframework.security.oauth2.provider.token.TokenStore;
  17. import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
  18. import org.springframework.test.context.ActiveProfiles;
  19. import org.springframework.test.context.junit4.SpringRunner;
  20. import org.springframework.test.web.servlet.MockMvc;
  21. import java.util.Optional;
  22. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.HEADER_AUTHORIZATION;
  23. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.bearer;
  24. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.obtainAccessToken;
  25. import static org.mockito.Mockito.when;
  26. import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
  27. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
  28. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  29. @RunWith(SpringRunner.class)
  30. @WebMvcTest(UserRestController.class)
  31. @ActiveProfiles(SpringProfiles.TEST)
  32. public class UserRestControllerTest {
  33. @Autowired
  34. private MockMvc mvc;
  35. @MockBean
  36. private UserService service;
  37. @Test
  38. public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception {
  39. mvc.perform(get("/api/users/me")).andExpect(status().isUnauthorized());
  40. }
  41. @Test
  42. public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception {
  43. String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD);
  44. when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer()));
  45. mvc.perform(get("/api/users/me")
  46. .header(HEADER_AUTHORIZATION, bearer(accessToken)))
  47. .andExpect(status().isOk())
  48. .andExpect(jsonPath("id").exists())
  49. .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL))
  50. .andExpect(jsonPath("roles").isArray())
  51. .andExpect(jsonPath("roles[0]").value("OFFICER"))
  52. ;
  53. }
  54. @TestConfiguration
  55. @Import(OAuth2ServerConfiguration.class)
  56. static class TestConfig {
  57. @Bean
  58. public UserDetailsService userDetailsService() {
  59. return new StubUserDetailsService();
  60. }
  61. @Bean
  62. public TokenStore tokenStore() {
  63. return new InMemoryTokenStore();
  64. }
  65. @Bean
  66. public SecurityConfiguration securityConfiguration() {
  67. return new SecurityConfiguration();
  68. }
  69. }
  70. }

上面的测试代码中,用到了下面几个类:

StubUserDetailsService.java

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.security.ApplicationUserDetails;
  3. import com.babyrabbit.demo.user.Users;
  4. import org.springframework.security.core.userdetails.UserDetailsService;
  5. import org.springframework.security.core.userdetails.UserDetails;
  6. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  7. public class StubUserDetailsService implements UserDetailsService {
  8. @Override
  9. public UserDetails loadUserByUsername(String username) throws
  10. UsernameNotFoundException {
  11. switch (username) {
  12. case Users.OFFICER_EMAIL:
  13. return new ApplicationUserDetails(Users.officer());
  14. case Users.CAPTAIN_EMAIL:
  15. return new ApplicationUserDetails(Users.captain());
  16. default:
  17. throw new UsernameNotFoundException(username);
  18. }
  19. }
  20. }

SecurityHelperForMockMvc.java

  1. package com.babyrabbit.demo.security;
  2. import org.springframework.boot.json.JacksonJsonParser;
  3. import org.springframework.test.web.servlet.MockMvc;
  4. import org.springframework.test.web.servlet.ResultActions;
  5. import org.springframework.util.LinkedMultiValueMap;
  6. import org.springframework.util.MultiValueMap;
  7. import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
  8. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
  9. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
  10. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  11. public class SecurityHelperForMockMvc {
  12. private static final String UNIT_TEST_CLIENT_ID = "test-client-id";
  13. private static final String UNIT_TEST_CLIENT_SECRET = "test-client-secret";
  14. public static final String HEADER_AUTHORIZATION = "Authorization";
  15. /**
  16. * Allows to get an access token for the given user in the context of a spring (unit)
  17. test
  18. * using MockMVC.
  19. *
  20. * @param mvc the MockMvc instance
  21. * @param username the username
  22. * @param password the password
  23. * @return the <code>access_token</code> to be used in the <code>Authorization</code>
  24. header
  25. * @throws Exception if no token could be obtained.
  26. */
  27. public static String obtainAccessToken(MockMvc mvc, String username, String password)
  28. throws Exception {
  29. MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
  30. params.add("grant_type", "password");
  31. params.add("client_id", UNIT_TEST_CLIENT_ID);
  32. params.add("client_secret", UNIT_TEST_CLIENT_SECRET);
  33. params.add("username", username);
  34. params.add("password", password);
  35. ResultActions result = mvc.perform(post("/oauth/token")
  36. .params(params)
  37. .with(httpBasic(UNIT_TEST_CLIENT_ID,UNIT_TEST_CLIENT_SECRET))
  38. .accept("application/json;charset=UTF-8"))
  39. .andExpect(status().isOk())
  40. .andExpect(content().contentType("application/json;charset=UTF-8"));
  41. String resultString = result.andReturn().getResponse().getContentAsString();
  42. JacksonJsonParser jsonParser = new JacksonJsonParser();
  43. return jsonParser.parseMap(resultString).get("access_token").toString();
  44. }
  45. public static String bearer(String accessToken) {
  46. return "Bearer " + accessToken;
  47. }
  48. }

UNIT_TEST_CLIENT_IDUNIT_TEST_CLIENT_SECRET的值要与application-test.properties里的client id, client secret一致。

POST 接口

为了能让mobile-app 用户创建账号,需要提供一个/api/users/ POST 接口。这个接口接收JSON类型的数据,来创建账号。

首先,为这个接口创建入参:

CreateOfficerParameters.java

  1. package com.babyrabbit.demo.web;
  2. import lombok.Data;
  3. import org.hibernate.validator.constraints.Email;
  4. import javax.validation.constraints.NotNull;
  5. import javax.validation.constraints.Size;
  6. @Data
  7. public class CreateOfficerParameters {
  8. @NotNull
  9. @Email
  10. private String email;
  11. @NotNull
  12. @Size(min = 6, max = 1000)
  13. private String password;
  14. }

在UserRestController里创建接口方法:

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.security.ApplicationUserDetails;
  3. import com.babyrabbit.demo.user.User;
  4. import com.babyrabbit.demo.user.UserNotFoundException;
  5. import com.babyrabbit.demo.user.UserService;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.http.HttpStatus;
  8. import org.springframework.security.core.annotation.AuthenticationPrincipal;
  9. import org.springframework.web.bind.annotation.*;
  10. import javax.validation.Valid;
  11. @RestController
  12. @RequestMapping("/api/users")
  13. public class UserRestController {
  14. private final UserService service;
  15. @Autowired
  16. public UserRestController(UserService service) {
  17. this.service = service;
  18. }
  19. @GetMapping("/me")
  20. public UserDto currentUser(@AuthenticationPrincipal ApplicationUserDetails userDetails) {
  21. User user = service.getUser(userDetails.getUserId())
  22. .orElseThrow(() -> new UserNotFoundException(userDetails.getUserId()));
  23. return UserDto.fromUser(user);
  24. }
  25. @PostMapping
  26. @ResponseStatus(HttpStatus.CREATED)
  27. public UserDto createOfficer(@Valid @RequestBody CreateOfficerParameters parameters)
  28. {
  29. User officer = service.createOfficer(parameters.getEmail(), parameters.getPassword());
  30. return UserDto.fromUser(officer);
  31. }
  32. }

下面就为这个接口构建测试代码:

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.conf.OAuth2ServerConfiguration;
  3. import com.babyrabbit.demo.conf.SecurityConfiguration;
  4. import com.babyrabbit.demo.user.UserService;
  5. import com.babyrabbit.demo.user.Users;
  6. import com.babyrabbit.demo.util.SpringProfiles;
  7. import com.fasterxml.jackson.databind.ObjectMapper;
  8. import org.junit.Test;
  9. import org.junit.runner.RunWith;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
  12. import org.springframework.boot.test.context.TestConfiguration;
  13. import org.springframework.boot.test.mock.mockito.MockBean;
  14. import org.springframework.context.annotation.Bean;
  15. import org.springframework.context.annotation.Import;
  16. import org.springframework.http.MediaType;
  17. import org.springframework.security.core.userdetails.UserDetailsService;
  18. import org.springframework.security.oauth2.provider.token.TokenStore;
  19. import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
  20. import org.springframework.test.context.ActiveProfiles;
  21. import org.springframework.test.context.junit4.SpringRunner;
  22. import org.springframework.test.web.servlet.MockMvc;
  23. import java.util.Optional;
  24. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.HEADER_AUTHORIZATION;
  25. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.bearer;
  26. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.obtainAccessToken;
  27. import static org.mockito.Mockito.verify;
  28. import static org.mockito.Mockito.when;
  29. import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
  30. import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
  31. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
  32. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  33. @RunWith(SpringRunner.class)
  34. @WebMvcTest(UserRestController.class)
  35. @ActiveProfiles(SpringProfiles.TEST)
  36. public class UserRestControllerTest {
  37. @Autowired
  38. private MockMvc mvc;
  39. @MockBean
  40. private UserService service;
  41. @Autowired
  42. private ObjectMapper objectMapper;
  43. @Test
  44. public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception {
  45. mvc.perform(get("/api/users/me")).andExpect(status().isUnauthorized());
  46. }
  47. @Test
  48. public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception {
  49. String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD);
  50. when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer()));
  51. mvc.perform(get("/api/users/me")
  52. .header(HEADER_AUTHORIZATION, bearer(accessToken)))
  53. .andExpect(status().isOk())
  54. .andExpect(jsonPath("id").exists())
  55. .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL))
  56. .andExpect(jsonPath("roles").isArray())
  57. .andExpect(jsonPath("roles[0]").value("OFFICER"))
  58. ;
  59. }
  60. @Test
  61. public void testCreateOfficer() throws Exception {
  62. String email = "wim.deblauwe@example.com";
  63. String password = "my-super-secret-pwd";
  64. CreateOfficerParameters parameters = new CreateOfficerParameters();
  65. parameters.setEmail(email);
  66. parameters.setPassword(password);
  67. when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email,password));
  68. mvc.perform(post("/api/users")
  69. .contentType(MediaType.APPLICATION_JSON_UTF8)
  70. .content(objectMapper.writeValueAsString(parameters)))
  71. .andExpect(status().isCreated())
  72. .andExpect(jsonPath("id").exists())
  73. .andExpect(jsonPath("email").value(email))
  74. .andExpect(jsonPath("roles").isArray())
  75. .andExpect(jsonPath("roles[0]").value("OFFICER"));
  76. verify(service).createOfficer(email, password);
  77. }
  78. @TestConfiguration
  79. @Import(OAuth2ServerConfiguration.class)
  80. static class TestConfig {
  81. @Bean
  82. public UserDetailsService userDetailsService() {
  83. return new StubUserDetailsService();
  84. }
  85. @Bean
  86. public TokenStore tokenStore() {
  87. return new InMemoryTokenStore();
  88. }
  89. @Bean
  90. public SecurityConfiguration securityConfiguration() {
  91. return new SecurityConfiguration();
  92. }
  93. }
  94. }

需要在Users中增加一个createOfficer方法:

  1. package com.babyrabbit.demo.user;
  2. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  3. import org.springframework.security.crypto.password.PasswordEncoder;
  4. import java.util.HashSet;
  5. import java.util.UUID;
  6. public class Users {
  7. private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
  8. public static final String OFFICER_EMAIL = "officer@example.com";
  9. public static final String OFFICER_PASSWORD = "officer";
  10. public static final String CAPTAIN_EMAIL = "captain@example.com";
  11. public static final String CAPTAIN_PASSWORD = "captain";
  12. private static User OFFICER = User.createOfficer(newRandomId(),
  13. OFFICER_EMAIL,
  14. PASSWORD_ENCODER.encode(OFFICER_PASSWORD));
  15. private static User CAPTAIN = User.createCaptain(newRandomId(),
  16. CAPTAIN_EMAIL,
  17. PASSWORD_ENCODER.encode(CAPTAIN_PASSWORD));
  18. public static UserId newRandomId() {
  19. return new UserId(UUID.randomUUID());
  20. }
  21. public static User newRandomOfficer() {
  22. return newRandomOfficer(newRandomId());
  23. }
  24. public static User newRandomOfficer(UserId userId) {
  25. String uniqueId = userId.asString().substring(0, 5);
  26. return User.createOfficer(userId,
  27. "user-" + uniqueId + "@example.com",
  28. PASSWORD_ENCODER.encode("user"));
  29. }
  30. public static User officer() {
  31. return OFFICER;
  32. }
  33. public static User captain() {
  34. return CAPTAIN;
  35. }
  36. private Users() {
  37. }
  38. public static User newOfficer(String email, String password) {
  39. HashSet<UserRole> roles = new HashSet<>();
  40. roles.add(UserRole.OFFICER);
  41. return new User(new UserId(UUID.randomUUID()),email,PASSWORD_ENCODER.encode(password),roles);
  42. }
  43. }

执行这个测试用例会返回"401 Unauthorized"错误,这是因为Spring Security Oauth 资源服务中配置的队所有/api接口访问都需要认证,而创建账号时是无法认证的,因此,需要修改资源服务的配置,允许/api/users的post请求访问:

OAuth2ServerConfiguration.java

  1. package com.babyrabbit.demo.conf;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.beans.factory.annotation.Value;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.http.HttpMethod;
  6. import org.springframework.security.authentication.AuthenticationManager;
  7. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  8. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  9. import org.springframework.security.core.userdetails.UserDetailsService;
  10. import org.springframework.security.crypto.password.PasswordEncoder;
  11. import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
  12. import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
  13. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
  14. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
  15. import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
  16. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
  17. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
  18. import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
  19. import org.springframework.security.oauth2.provider.token.TokenStore;
  20. /**
  21. * Created by kevin on 2018/9/14.
  22. */
  23. @Configuration
  24. public class OAuth2ServerConfiguration {
  25. @Value("${resource_id}")
  26. private String resourceId;
  27. @Configuration
  28. @EnableAuthorizationServer
  29. protected class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
  30. @Autowired
  31. private AuthenticationManager authenticationManager;
  32. @Autowired
  33. private UserDetailsService userDetailsService;
  34. @Autowired
  35. private PasswordEncoder passwordEncoder;
  36. @Autowired
  37. private TokenStore tokenStore;
  38. @Autowired
  39. private SecurityConfiguration securityConfiguration;
  40. @Override
  41. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  42. security.passwordEncoder(passwordEncoder);
  43. }
  44. @Override
  45. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  46. clients.inMemory()
  47. .withClient(securityConfiguration.getMobileAppClientId())
  48. .authorizedGrantTypes("password", "refresh_token")
  49. .scopes("mobile_app")
  50. .resourceIds(resourceId)
  51. .secret(passwordEncoder.encode(securityConfiguration.getMobileAppClientSecret()));
  52. }
  53. @Override
  54. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  55. endpoints.tokenStore(tokenStore)
  56. .authenticationManager(authenticationManager)
  57. .userDetailsService(userDetailsService);
  58. }
  59. }
  60. @Configuration
  61. @EnableResourceServer
  62. @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
  63. protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
  64. @Override
  65. public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
  66. resources.resourceId(resourceId);
  67. }
  68. @Override
  69. public void configure(HttpSecurity http) throws Exception {
  70. http.authorizeRequests()
  71. .antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll()
  72. .and()
  73. .antMatcher("/api/**").authorizeRequests()
  74. .antMatchers(HttpMethod.POST, "/api/users").permitAll()
  75. .anyRequest().authenticated();
  76. }
  77. }
  78. }

对测试代码重构

现在只有一个Controller,对应也只创建了一个Controller的测试代码,如果后面再有新的controller,也要创建新的controller测试代码,就会有一些重复代码。为了减少重复代码,下面对controller的测试代码做一点重构工作。

创建自己的注解

在控制器测试代码上有一组注解:

  1. @WebMvcTest(UserRestController.class)
  2. @ActiveProfiles(SpringProfiles.TEST)

这两行注解基本上所有的控制器测试类都要有。可以合并成自定义注解,用一行代码实现两行代码的效果。

BabyControllerTest .java

  1. package com.babyrabbit.demo.util;
  2. import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
  3. import org.springframework.core.annotation.AliasFor;
  4. import org.springframework.test.context.ActiveProfiles;
  5. import org.springframework.test.context.ContextConfiguration;
  6. import java.lang.annotation.Retention;
  7. import java.lang.annotation.RetentionPolicy;
  8. @Retention(RetentionPolicy.RUNTIME)
  9. @WebMvcTest
  10. @ContextConfiguration(classes = BabyControllerTestConfiguration.class)
  11. @ActiveProfiles(SpringProfiles.TEST)
  12. public @interface BabyControllerTest {
  13. @AliasFor(annotation = WebMvcTest.class, attribute = "value")
  14. Class<?>[] value() default {};
  15. @AliasFor(annotation = WebMvcTest.class, attribute = "controllers")
  16. Class<?>[] controllers() default {};
  17. }

这个注解是利用spring的meta-annotations特性,将多个注解组合成一个注解。

BabyControllerTestConfiguration.java

  1. package com.babyrabbit.demo.util;
  2. import com.babyrabbit.demo.conf.OAuth2ServerConfiguration;
  3. import com.babyrabbit.demo.conf.SecurityConfiguration;
  4. import com.babyrabbit.demo.web.StubUserDetailsService;
  5. import org.springframework.boot.test.context.TestConfiguration;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Import;
  8. import org.springframework.security.core.userdetails.UserDetailsService;
  9. import org.springframework.security.oauth2.provider.token.TokenStore;
  10. import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
  11. @TestConfiguration
  12. @Import(OAuth2ServerConfiguration.class)
  13. public class BabyControllerTestConfiguration {
  14. @Bean
  15. public UserDetailsService userDetailsService() {
  16. return new StubUserDetailsService();
  17. }
  18. @Bean
  19. public TokenStore tokenStore() {
  20. return new InMemoryTokenStore();
  21. }
  22. @Bean
  23. public SecurityConfiguration securityConfiguration() {
  24. return new SecurityConfiguration();
  25. }
  26. }

这个类就是前面在UserRestControllerTest.java中的那个静态类TestConfig,把这个类拿出来放到单独的文件里,以便在多个ControllerTest中重用。

这样,UserRestControllerTest中就需要把TestConfig静态类移除:

UserRestControllerTest.java

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.conf.OAuth2ServerConfiguration;
  3. import com.babyrabbit.demo.conf.SecurityConfiguration;
  4. import com.babyrabbit.demo.user.UserService;
  5. import com.babyrabbit.demo.user.Users;
  6. import com.babyrabbit.demo.util.BabyControllerTest;
  7. import com.babyrabbit.demo.util.SpringProfiles;
  8. import com.fasterxml.jackson.databind.ObjectMapper;
  9. import org.junit.Test;
  10. import org.junit.runner.RunWith;
  11. import org.springframework.beans.factory.annotation.Autowired;
  12. import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
  13. import org.springframework.boot.test.mock.mockito.MockBean;
  14. import org.springframework.http.MediaType;
  15. import org.springframework.test.context.ActiveProfiles;
  16. import org.springframework.test.context.junit4.SpringRunner;
  17. import org.springframework.test.web.servlet.MockMvc;
  18. import java.util.Optional;
  19. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.HEADER_AUTHORIZATION;
  20. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.bearer;
  21. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.obtainAccessToken;
  22. import static org.mockito.Mockito.verify;
  23. import static org.mockito.Mockito.when;
  24. import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
  25. import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
  26. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
  27. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  28. @RunWith(SpringRunner.class)
  29. @BabyControllerTest(UserRestController.class)
  30. public class UserRestControllerTest {
  31. @Autowired
  32. private MockMvc mvc;
  33. @MockBean
  34. private UserService service;
  35. @Autowired
  36. private ObjectMapper objectMapper;
  37. @Test
  38. public void givenNotAuthenticated_whenAskingMyDetails_forbidden() throws Exception {
  39. mvc.perform(get("/api/users/me")).andExpect(status().isUnauthorized());
  40. }
  41. @Test
  42. public void givenAuthenticatedAsOfficer_whenAskingMyDetails_detailsReturned() throws Exception {
  43. String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users.OFFICER_PASSWORD);
  44. when(service.getUser(Users.officer().getId())).thenReturn(Optional.of(Users.officer()));
  45. mvc.perform(get("/api/users/me")
  46. .header(HEADER_AUTHORIZATION, bearer(accessToken)))
  47. .andExpect(status().isOk())
  48. .andExpect(jsonPath("id").exists())
  49. .andExpect(jsonPath("email").value(Users.OFFICER_EMAIL))
  50. .andExpect(jsonPath("roles").isArray())
  51. .andExpect(jsonPath("roles[0]").value("OFFICER"))
  52. ;
  53. }
  54. @Test
  55. public void testCreateOfficer() throws Exception {
  56. String email = "wim.deblauwe@example.com";
  57. String password = "my-super-secret-pwd";
  58. CreateOfficerParameters parameters = new CreateOfficerParameters();
  59. parameters.setEmail(email);
  60. parameters.setPassword(password);
  61. when(service.createOfficer(email, password)).thenReturn(Users.newOfficer(email,password));
  62. mvc.perform(post("/api/users")
  63. .contentType(MediaType.APPLICATION_JSON_UTF8)
  64. .content(objectMapper.writeValueAsString(parameters)))
  65. .andExpect(status().isCreated())
  66. .andExpect(jsonPath("id").exists())
  67. .andExpect(jsonPath("email").value(email))
  68. .andExpect(jsonPath("roles").isArray())
  69. .andExpect(jsonPath("roles[0]").value("OFFICER"));
  70. verify(service).createOfficer(email, password);
  71. }
  72. }

使用MySQL数据库

前面的代码,都是在内存里模拟用户数据,实际项目里,还是会需要用到数据库的。这里就用MySQL数据库来继续开发这个工程。

配置数据库信息

由于我打算将数据库和API服务放在一个服务器上,将使用spring profile配合来配置数据库连接信息。

设定访问localhost 数据库的profile为 local,首先需要创建application-local.properties,配置数据库访问信息:

  1. copsboot-security.mobile-app-client-id=baby-mobile-client
  2. copsboot-security.mobile-app-client-secret=ccUyb6vS4S8nxfbKPCrN
  3. spring.datasource.url=jdbc:mysql://localhost/babydb
  4. spring.datasource.driverClassName=com.mysql.jdbc.Driver
  5. spring.datasource.username=dbadmin
  6. spring.datasource.password=dbadmin
  7. spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
  8. spring.jpa.hibernate.ddl-auto=none

这个文件中包含了敏感信息,所以不要把这个文件提交到git上。推荐个好的实践,提交application-local.properties.template作为样例文件,当然,要把其中的敏感信息隐藏掉

创建数据库

为了对数据库变更也能做版本管理,spring boot 支持两个库: Flyway 和 Liquibase,在spring boot启动时,自动执行SQL 脚本确保数据库是最新的。

这两个库最大的区别是:Flyway 使用的是SQL脚本,而Liquibase使用的是XML语法。这里我使用的是Flyway。首先需要在工程的pom.xml中添加依赖:

  1. <dependency>
  2. <groupId>org.flywaydb</groupId>
  3. <artifactId>flyway-core</artifactId>
  4. </dependency>

在src/main/resources下创建db/migration目录作为Flyway的工作目录。在migration目录里,再创建一个h2目录,一个mysql目录,分别用来放数据库脚本。

实际的目录结构如下:

  1. pom.xml
  2. mvnw
  3. mvnw.cmd
  4. src
  5. |-- main
  6. |-- java
  7. |-- com.babyrabbit.demo
  8. |-- DemoApplication
  9. |-- resources
  10. |-- application.properties
  11. |-- db
  12. |-- migration
  13. |-- h2
  14. |-- V1.0.0.1__authentication.sql
  15. |-- mysql
  16. |-- V1.0.0.1__authentication.sql
  17. |-- V1.0.0.2__users.sql
  18. |-- test
  19. |-- java
  20. |-- com.babyrabbit.demo
  21. |-- ApplicationTests

sql文件的名字非常重要,Flyway将使用这个名字来决定这些脚本的执行顺序。

创建数据表的脚本可以自己手写,可以让jpa自动生成创建脚本:

application-local.properties

  1. spring.jpa.properties.javax.persistence.schema-generation.create-source=metadata
  2. spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create
  3. spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=create.sql

当选用local作为激活的profile,启动程序,spring boot会自动创建create.sql文件,包含所有的创建脚本。

把create.sql 脚本复制到mysql目录下,命名为V1.0.0.2_users.sql , 在application-local.properties添加配置,告诉Flpyway 脚本的相对位置:

spring.flyway.locations=classpath:db/migration/mysql

对于dev 这个profile,需要配置的是application-dev.properties:

  1. spring.flyway.locations=classpath:db/migration/h2
  2. spring.jpa.hibernate.ddl-auto=create-drop

初始化数据库

当使用dev这个profile 启动服务时, 会触发spring boot执行DevelopmentDbInitializer, 创建测试用户。如果想同时在MySQL中创建默认用户,可以在启动时候,同时使用 dev, local作为激活的profile。 激活的profile的顺序很重要,dev在前, local在后,这样local可以重写数据库连接属性。

Flyway将在库里创建一个schema_version表,用来记录那些更新已经执行,那些还没有执行。

注意: 一旦发布了一个数据库更新脚本,并让flyway执行了,就不要更改这个文件了,否则会导致flyway拒绝执行任何脚本,最终导致程序启动失败。如果要修改数据库,就新创建一个脚本,不要在已有脚本中修改。

校验

spring中可以使用javax.validation.constrants包里的校验注解,也可以使用org.hibernate.validation.constrants包里校验注解。

这些校验注解都是用来校验对象里的字段的。在controller中,启用一个校验,使用@Valid 注解,则框架会根据数据对象里的校验注解对数据进行校验。

使用exception handler处理校验错误

例外处理如果跟正常业务逻辑混在一起,不但让代码乱,有很多重复代码,还可能引入bug。spring提供一个@ExceptionHandler注解,来单独创建例外处理,这样就可以把例外处理的代码集中放置。

要处理Controller中抛出的例外,需要使用@ControllerAdvice注解,捕捉Controller中的例外:

RestControllerExceptionHandler.java

  1. package com.babyrabbit.demo.web;
  2. import org.springframework.http.HttpStatus;
  3. import org.springframework.web.bind.MethodArgumentNotValidException;
  4. import org.springframework.web.bind.annotation.ControllerAdvice;
  5. import org.springframework.web.bind.annotation.ExceptionHandler;
  6. import org.springframework.web.bind.annotation.ResponseBody;
  7. import org.springframework.web.bind.annotation.ResponseStatus;
  8. import java.util.Collections;
  9. import java.util.List;
  10. import java.util.Map;
  11. import java.util.stream.Collectors;
  12. @ControllerAdvice
  13. public class RestControllerExceptionHandler {
  14. @ExceptionHandler
  15. @ResponseBody
  16. @ResponseStatus(HttpStatus.BAD_REQUEST)
  17. public Map<String, List<FieldErrorResponse>> handle(MethodArgumentNotValidException exception) {
  18. return error(exception.getBindingResult()
  19. .getFieldErrors()
  20. .stream()
  21. .map(fieldError -> new FieldErrorResponse(fieldError.getField(),
  22. fieldError.getDefaultMessage()))
  23. .collect(Collectors.toList()));
  24. }
  25. private Map<String, List<FieldErrorResponse>> error(List<FieldErrorResponse> errors)
  26. {
  27. return Collections.singletonMap("errors", errors);
  28. }
  29. }

对上面代码的一点解释:

  • @ControllerAdvice注解标注这个类的代码是应用到这个工程的所有controller上。
  • @ExceptionHandler注解标注当controller抛出例外时,调用这个方法

上面代码中,用到了一个数据容器,当校验失败时,用其保持校验失败的字段名及校验失败的错误信息:

FieldErrorResponse.java

  1. package com.babyrabbit.demo.web;
  2. import lombok.Value;
  3. @Value
  4. public class FieldErrorResponse {
  5. private String fieldName;
  6. private String errorMessage;
  7. }

自定义字段校验器

为了演示如何使用自定义字段校验器,下面创建了Report模块。

ReportRestController.java

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.report.ReportService;
  3. import com.babyrabbit.demo.security.ApplicationUserDetails;
  4. import org.springframework.http.HttpStatus;
  5. import org.springframework.security.core.annotation.AuthenticationPrincipal;
  6. import org.springframework.web.bind.annotation.*;
  7. import javax.validation.Valid;
  8. @RestController
  9. @RequestMapping("/api/reports")
  10. public class ReportRestController {
  11. private final ReportService service;
  12. public ReportRestController(ReportService service) {
  13. this.service = service;
  14. }
  15. @PostMapping
  16. @ResponseStatus(HttpStatus.CREATED)
  17. public ReportDto createReport(@AuthenticationPrincipal ApplicationUserDetails userDetails,
  18. @Valid @RequestBody CreateReportParameters parameters) {
  19. return ReportDto.fromReport(service.createReport(userDetails.getUserId(),
  20. parameters.getDateTime(),
  21. parameters.getDescription()));
  22. }
  23. }

Report.java

  1. package com.babyrabbit.demo.report;
  2. import com.babyrabbit.demo.orm.jpa.AbstractEntity;
  3. import com.babyrabbit.demo.user.User;
  4. import com.babyrabbit.demo.util.ArtifactForFramework;
  5. import lombok.Getter;
  6. import javax.persistence.Entity;
  7. import javax.persistence.ManyToOne;
  8. import java.time.ZonedDateTime;
  9. @Getter
  10. @Entity
  11. public class Report extends AbstractEntity<ReportId> {
  12. @ManyToOne
  13. private User reporter;
  14. private ZonedDateTime dateTime;
  15. private String description;
  16. @ArtifactForFramework
  17. protected Report() {
  18. }
  19. public Report(ReportId id, User reporter, ZonedDateTime dateTime, String description)
  20. {
  21. super(id);
  22. this.reporter = reporter;
  23. this.dateTime = dateTime;
  24. this.description = description;
  25. }
  26. }

ReportId.java

  1. package com.babyrabbit.demo.report;
  2. import com.babyrabbit.demo.orm.jpa.AbstractEntityId;
  3. import java.util.UUID;
  4. /**
  5. * Created by kevin on 2018/9/14.
  6. */
  7. public class ReportId extends AbstractEntityId<UUID> {
  8. protected ReportId(){
  9. }
  10. public ReportId(UUID id){
  11. super(id);
  12. }
  13. @Override
  14. public String asString() {
  15. return super.getId().toString();
  16. }
  17. }

ReportRepository.java

  1. package com.babyrabbit.demo.report;
  2. import org.springframework.data.repository.CrudRepository;
  3. import java.util.UUID;
  4. /**
  5. * Created by kevin on 2018/9/14.
  6. */
  7. public interface ReportRepository extends CrudRepository<Report, UUID>,ReportRepositoryCustom{
  8. }

ReportRepositoryCustomer.java

  1. package com.babyrabbit.demo.report;
  2. import com.babyrabbit.demo.user.UserId;
  3. /**
  4. * Created by kevin on 2018/9/14.
  5. */
  6. public interface ReportRepositoryCustom {
  7. ReportId nextId();
  8. }

ReportRepositoryImpl.java

  1. package com.babyrabbit.demo.report;
  2. import com.babyrabbit.demo.orm.jpa.UniqueIdGenerator;
  3. import java.util.UUID;
  4. /**
  5. * Created by kevin on 2018/9/14.
  6. */
  7. public class ReportRepositoryImpl implements ReportRepositoryCustom{
  8. private final UniqueIdGenerator<UUID> generator;
  9. public ReportRepositoryImpl(UniqueIdGenerator<UUID> generator) {
  10. this.generator = generator;
  11. }
  12. @Override
  13. public ReportId nextId() {
  14. return new ReportId(generator.getNextUniqueId());
  15. }
  16. }

ReportService.java

  1. package com.babyrabbit.demo.report;
  2. import com.babyrabbit.demo.user.UserId;
  3. import java.time.ZonedDateTime;
  4. /**
  5. * Created by kevin on 2018/9/14.
  6. */
  7. public interface ReportService {
  8. Report createReport(UserId userId, ZonedDateTime dateTime, String description);
  9. }

ReportServiceImpl.java

  1. package com.babyrabbit.demo.report;
  2. import com.babyrabbit.demo.user.User;
  3. import com.babyrabbit.demo.user.UserId;
  4. import com.babyrabbit.demo.user.UserNotFoundException;
  5. import com.babyrabbit.demo.user.UserRepository;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import java.time.ZonedDateTime;
  8. /**
  9. * Created by kevin on 2018/9/14.
  10. */
  11. public class ReportServiceImpl implements ReportService{
  12. private final ReportRepository reportRepository;
  13. private final UserRepository userRepository;
  14. @Autowired
  15. public ReportServiceImpl(ReportRepository reportRepository, UserRepository userRepository){
  16. this.reportRepository = reportRepository;
  17. this.userRepository = userRepository;
  18. }
  19. @Override
  20. public Report createReport(UserId userId, ZonedDateTime dateTime, String description) {
  21. User user = userRepository.findById(userId.getId()).orElseThrow(() -> new UserNotFoundException(userId));
  22. Report report = Reports.createReport(reportRepository.nextId(), user,dateTime,description);
  23. return reportRepository.save(report);
  24. }
  25. }

ReportDto.java

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.report.Report;
  3. import com.babyrabbit.demo.report.ReportId;
  4. import lombok.Value;
  5. import java.time.ZonedDateTime;
  6. @Value
  7. public class ReportDto {
  8. private ReportId id;
  9. private String reporter;
  10. private ZonedDateTime dateTime;
  11. private String description;
  12. public static ReportDto fromReport(Report report) {
  13. return new ReportDto(report.getId(),
  14. report.getReporter().getEmail(),
  15. report.getDateTime(),
  16. report.getDescription());
  17. }
  18. }

最后为上面的代码创建测试代码:

ReportRestControllerTest.java

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.report.Report;
  3. import com.babyrabbit.demo.report.ReportId;
  4. import com.babyrabbit.demo.report.ReportService;
  5. import com.babyrabbit.demo.user.Users;
  6. import com.babyrabbit.demo.util.BabyControllerTest;
  7. import com.fasterxml.jackson.databind.ObjectMapper;
  8. import org.junit.Test;
  9. import org.junit.runner.RunWith;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.boot.test.mock.mockito.MockBean;
  12. import org.springframework.http.MediaType;
  13. import org.springframework.test.context.junit4.SpringRunner;
  14. import org.springframework.test.web.servlet.MockMvc;
  15. import java.time.ZonedDateTime;
  16. import java.util.UUID;
  17. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.HEADER_AUTHORIZATION;
  18. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.bearer;
  19. import static com.babyrabbit.demo.security.SecurityHelperForMockMvc.obtainAccessToken;
  20. import static org.mockito.ArgumentMatchers.any;
  21. import static org.mockito.ArgumentMatchers.eq;
  22. import static org.mockito.Mockito.when;
  23. import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
  24. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
  25. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
  26. @RunWith(SpringRunner.class)
  27. @BabyControllerTest(ReportRestController.class)
  28. public class ReportRestControllerTest {
  29. @Autowired
  30. private MockMvc mvc;
  31. @Autowired
  32. private ObjectMapper objectMapper;
  33. @MockBean
  34. private ReportService service;
  35. @Test
  36. public void officerIsAbleToPostAReport() throws Exception {
  37. String accessToken = obtainAccessToken(mvc, Users.OFFICER_EMAIL, Users
  38. .OFFICER_PASSWORD);
  39. ZonedDateTime dateTime = ZonedDateTime.parse("2018-04-11T22:59:03.189+02:00");
  40. String description = "This is a test report description.";
  41. CreateReportParameters parameters = new CreateReportParameters(dateTime,
  42. description);
  43. when(service.createReport(eq(Users.officer().getId()), any(ZonedDateTime.class),
  44. eq(description)))
  45. .thenReturn(new Report(new ReportId(UUID.randomUUID()), Users.officer(),
  46. dateTime, description));
  47. mvc.perform(post("/api/reports")
  48. .header(HEADER_AUTHORIZATION, bearer(accessToken))
  49. .contentType(MediaType.APPLICATION_JSON_UTF8)
  50. .content(objectMapper.writeValueAsString(parameters)))
  51. .andExpect(status().isCreated())
  52. .andExpect(jsonPath("id").exists())
  53. .andExpect(jsonPath("reporter").value(Users.OFFICER_EMAIL))
  54. .andExpect(jsonPath("dateTime").value("2018-04-11T22:59:03.189+02:00"))
  55. .andExpect(jsonPath("description").value(description));
  56. }
  57. }

下面进入正题, 针对description字段,校验内容中是否有敏感词。这个校验需要自定义一个校验器ValidReportDescription:

ValidReportDescription.java

  1. package com.babyrabbit.demo.web;
  2. import javax.validation.Constraint;
  3. import javax.validation.Payload;
  4. import java.lang.annotation.ElementType;
  5. import java.lang.annotation.Retention;
  6. import java.lang.annotation.RetentionPolicy;
  7. import java.lang.annotation.Target;
  8. @Target(ElementType.FIELD)
  9. @Retention(RetentionPolicy.RUNTIME)
  10. @Constraint(validatedBy = {ReportDescriptionValidator.class})
  11. public @interface ValidReportDescription {
  12. String message() default "Invalid report description";
  13. Class<?>[] groups() default {};
  14. Class<? extends Payload>[] payload() default {};
  15. }

这个注解用来在description字段上添加校验标记。

ReportDescriptionValidator.java

  1. package com.babyrabbit.demo.web;
  2. import javax.validation.ConstraintValidator;
  3. import javax.validation.ConstraintValidatorContext;
  4. public class ReportDescriptionValidator implements ConstraintValidator<ValidReportDescription, String> {
  5. @Override
  6. public void initialize(ValidReportDescription constraintAnnotation) {
  7. }
  8. @Override
  9. public boolean isValid(String value, ConstraintValidatorContext context) {
  10. boolean result = true;
  11. if (!value.toLowerCase().contains("suspect")) {
  12. result = false;
  13. }
  14. return result;
  15. }
  16. }

这个类实现了真正的校验逻辑。

下面,就可以将这个自定义的字段校验器应用到代码中:

CreateReportParameters.java

  1. package com.babyrabbit.demo.web;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. import java.time.ZonedDateTime;
  6. @Data
  7. @AllArgsConstructor
  8. @NoArgsConstructor
  9. public class CreateReportParameters {
  10. private ZonedDateTime dateTime;
  11. @ValidReportDescription
  12. private String description;
  13. }

将自定义对象校验器注册玮Spring服务

普通的校验注解只是做前端校验,并没有深入到后端数据库或业务层面上的校验。如果创建一个User的时候,需要深入到数据库查重,这时候需要调用UserService来访问数据,执行业务逻辑。UserService是Spring管理的Service,因此要求校验器也要被Spring管理,才能实现注入。

下面先创建校验器注解类:

ValidCreateUserParameters.java

  1. package com.babyrabbit.demo.web;
  2. import javax.validation.Constraint;
  3. import javax.validation.Payload;
  4. import java.lang.annotation.ElementType;
  5. import java.lang.annotation.Retention;
  6. import java.lang.annotation.RetentionPolicy;
  7. import java.lang.annotation.Target;
  8. @Target(ElementType.TYPE)
  9. @Retention(RetentionPolicy.RUNTIME)
  10. @Constraint(validatedBy = {CreateUserParametersValidator.class})
  11. public @interface ValidCreateUserParameters {
  12. String message() default "Invalid user";
  13. Class<?>[] groups() default {};
  14. Class<? extends Payload>[] payload() default {};
  15. }

CreateUserParametersValidator 是校验逻辑实现类。

CreateUserParametersValidator.java

  1. package com.babyrabbit.demo.web;
  2. import com.babyrabbit.demo.user.UserService;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import javax.validation.ConstraintValidator;
  5. import javax.validation.ConstraintValidatorContext;
  6. public class CreateUserParametersValidator implements ConstraintValidator<ValidCreateUserParameters, CreateOfficerParameters> {
  7. private final UserService userService;
  8. @Autowired
  9. public CreateUserParametersValidator(UserService userService) {
  10. this.userService = userService;
  11. }
  12. @Override
  13. public void initialize(ValidCreateUserParameters constraintAnnotation) {
  14. }
  15. @Override
  16. public boolean isValid(CreateOfficerParameters userParameters,ConstraintValidatorContext context) {
  17. boolean result = true;
  18. if (userService.findUserByEmail(userParameters.getEmail()).isPresent()) {
  19. context.buildConstraintViolationWithTemplate(
  20. "There is already a user with the given email address.")
  21. .addPropertyNode("email").addConstraintViolation();
  22. result = false;
  23. }
  24. return result;
  25. }
  26. }

在上面的代码中,注入了UserService。

这样就实现了调用Spring Service做逻辑校验。

OAuth Database Schema

MySQL

  1. CREATE TABLE oauth_client_details (
  2. client_id VARCHAR(255) PRIMARY KEY,
  3. resource_ids VARCHAR(255),
  4. client_secret VARCHAR(255),
  5. scope VARCHAR(255),
  6. authorized_grant_types VARCHAR(255),
  7. web_server_redirect_uri VARCHAR(255),
  8. authorities VARCHAR(255),
  9. access_token_validity INTEGER,
  10. refresh_token_validity INTEGER,
  11. additional_information VARCHAR(4096),
  12. autoapprove TINYINT
  13. );
  14. CREATE TABLE oauth_client_token (
  15. token_id VARCHAR(255),
  16. token BLOB,
  17. authentication_id VARCHAR(255) PRIMARY KEY,
  18. user_name VARCHAR(255),
  19. client_id VARCHAR(255)
  20. );
  21. CREATE TABLE oauth_access_token (
  22. token_id VARCHAR(255),
  23. token BLOB,
  24. authentication_id VARCHAR(255) PRIMARY KEY,
  25. user_name VARCHAR(255),
  26. client_id VARCHAR(255),
  27. authentication BLOB,
  28. refresh_token VARCHAR(255)
  29. );
  30. CREATE TABLE oauth_refresh_token (
  31. token_id VARCHAR(255),
  32. token BLOB,
  33. authentication BLOB
  34. );
  35. CREATE TABLE oauth_code (
  36. activationCode VARCHAR(255),
  37. authentication BLOB
  38. );

H2

  1. CREATE TABLE oauth_client_details (
  2. client_id VARCHAR(255) PRIMARY KEY,
  3. resource_ids VARCHAR(255),
  4. client_secret VARCHAR(255),
  5. scope VARCHAR(255),
  6. authorized_grant_types VARCHAR(255),
  7. web_server_redirect_uri VARCHAR(255),
  8. authorities VARCHAR(255),
  9. access_token_validity INTEGER,
  10. refresh_token_validity INTEGER,
  11. additional_information VARCHAR(4096),
  12. autoapprove VARCHAR(255)
  13. );
  14. CREATE TABLE oauth_client_token (
  15. token_id VARCHAR(255),
  16. token BLOB,
  17. authentication_id VARCHAR(255) PRIMARY KEY,
  18. user_name VARCHAR(255),
  19. client_id VARCHAR(255)
  20. );
  21. CREATE TABLE oauth_access_token (
  22. token_id VARCHAR(255),
  23. token BLOB,
  24. authentication_id VARCHAR(255) PRIMARY KEY,
  25. user_name VARCHAR(255),
  26. client_id VARCHAR(255),
  27. authentication BLOB,
  28. refresh_token VARCHAR(255)
  29. );
  30. CREATE TABLE oauth_refresh_token (
  31. token_id VARCHAR(255),
  32. token BLOB,
  33. authentication BLOB
  34. );
  35. CREATE TABLE oauth_code (
  36. activationCode VARCHAR(255),
  37. authentication BLOB
  38. );

转载于:https://my.oschina.net/ez8life/blog/2051291

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/68967
推荐阅读
相关标签
  

闽ICP备14008679号