如何使用Spring Boot 2.x构建Web服务

译文 精选
开发 前端
想用Spring Boot创建一个Web服务应用程序吗?试试从这个代码结构模板开始吧!开发Web服务应用程序

译者 | 卢鑫旺

审校 | 梁策 孙淑娟

架构:

  • MVC架构
  • 基于JWT的身份认证
  • Spring Data (JPA)
  • 应用用户密码加密
  • 数据库密码加密
  • SQL Server
  • Slf4j
  • 基于Swagger的API文档

库:

  • 应用源代码
  • 数据库的SQL脚本以及关键数据
  • 包含数据库配置信息的DB.txt文件
  • 用于测试Web服务的Postman JSON脚本

运行应用的步骤

  • 安装JDK11或最新版本
  • 克隆项目库到本地
  • Git地址:https://github.com/VishnuViswam/sample-web-service.git
  • 安装SQL server 2012
  • 创建应用数据库和用户
  • 插入数据库密钥数据
  • 将数据库密码的解码密钥添加到系统变量,它位于DB.txt文件中
  • 有时可能需要重新启动窗口以获取更新后的系统变量
  • 运行项目源代码
  • 导入预先提供的postman JSON脚本到postman客户端,调用Web服务

关于项目配置

Web服务声明

应用程序的每个Web服务都将在controller层中声明。

示例

1.@RequestMapping("/api/v1/user")
2.@RestController
3.@Validated
4.public class UserController {
5.
6.    private static final Logger logger = LoggerFactory.getLogger(UserController.class);
7.
8.    @Autowired
9.    private GeneralServices generalServices;
10.
11.    @Autowired
12.    private UserService userService;
13.
14.    /**
15.     * Web service to create new user
16.     *
17.     * @param httpServletRequest
18.     * @param user
19.     * @return
20.     */
21.    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUEproduces = MediaType.APPLICATION_JSON_VALUE)
22.    public ResponseEntity<Object> createUser(HttpServletRequest httpServletRequest,
23.                                             @Valid @RequestBody UserCreateModel user) {
24.        logger.debug("<--- Service to save new user request : received --->");
25.        ApiSuccessResponse apiResponse = userService.createUser(usergeneralServices.getApiRequestedUserId(httpServletRequest));
26.        logger.debug("<--- Service to save new user response : given --->");
27.        return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);
28.
29.    }
30.
31.}
  • @RequestMapping("/api/v1/user")注解用来声明Web服务的类别
  • @RestController注解配置该类来接收Restful的 Web服务调用
  • @PostMapping()注解决定了HTTP请求类型
  • consume和consume标记来确定HTTP请求和响应的内容类型

通过controller层,API请求将被带到服务层。所有业务逻辑都将在这里处理,然后它将使用JPA与数据库通信。

通用错误处理

每当异常发生时,它将从相应的类抛出,并在CommonExceptionHandlingController中处理。我们必须分别处理每种异常类型。这个功能是在ControllerAdvice注解的帮助下执行的。

示例

1.@ControllerAdvice
2.public class CommonExceptionHandlingController extends ResponseEntityExceptionHandler {
3.
4.    private static final Logger logger = 
5.          LoggerFactory.getLogger(CommonExceptionHandlingController.class);
6.
7.    @Override
8.    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException httpRequestMethodNotSupportedException,
9.                                                                         HttpHeaders headersHttpStatus statusWebRequest request) {
10.        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiErrorResponse(Constants.WRONG_HTTP_METHOD,
11.                Constants.WRONG_HTTP_METHOD_ERROR_MESSAGECalendar.getInstance().getTimeInMillis()));
12.    }
13.
14.    @Override
15.    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException methodArgumentNotValidException,
16.                                                                  HttpHeaders headersHttpStatus statusWebRequest request) {
17.        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ApiErrorResponse(Constants.MANDATORY_FIELDS_ARE_NOT_PRESENT_CODE,
18.                Constants.MANDATORY_FIELDS_ARE_NOT_PRESENT_ERROR_MESSAGECalendar.getInstance().getTimeInMillis()));
19.    }

Spring Data(JPA)配置

  • 应用程序与数据库的所有交互都将由JPA处理
  • JPA将为应用程序中的所有逻辑对象提供一个Entity类和一个相应的Repository接口

Entity类

1.@Entity
2.@Table(name = "tbl_users")
3.public class Users implements Serializable {
4.    private static final long serialVersionUID = 1L;
5.
6.    @Id
7.    @GeneratedValue(strategy = GenerationType.IDENTITY)
8.    @Column(name = "id"columnDefinition = "bigint")
9.    private Long id;
10.
11.    @OneToOne(fetch = FetchType.EAGER)
12.    @JoinColumn(name = "user_account_id"columnDefinition = "bigint"nullable = false)
13.    private UserAccounts userAccount;

Repository接口

1.public interface UserRepository extends JpaRepository<UsersLong> {
2.
3.    /**
4.     * To find user object using username
5.     *
6.     * @param username
7.     * @return
8.     */
9.    Users findByUserAccountUsername(String username);
  • 其他的JPA配置将会在application.properties文件中完成

在application.properties中的JPA数据库配置

1.spring.jpa.show-sql=false
2.spring.jpa.hibernate.dialect=org.hibernate.dialect.SQLServer2012Dialect
3.spring.jpa.hibernate.ddl-auto = update
4.
5.spring.jpa.properties.hibernate.show_sql=false
6.spring.jpa.properties.hibernate.format_sql=false
7.spring.jpa.properties.hibernate.use_sql=true
8.spring.jpa.open-in-view=false
9.spring.jpa.properties.hibernate.hbm2ddl.auto=update
10.spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
11.spring.jpa.hibernate.connection.provider_class=org.hibernate.hikaricp.internal.HikariCPConnectionProvider

数据库配置

  • 数据库名称写在application.properties文件中
  • 其他信息(比如URL连接地址和账号密码)将写到另外两个不同属性文件中

application-dev.properties

此处写开发环境的配置信息

application-pro.properties

此处写生产环境的配置信息

spring.profiles.active=dev
  • 上述提到的属性配置写到application.properties文件中
  • 它将决定系统使用哪个子配置文件(开发环境还是生产环境)

application.properties

1.#DB config
2.spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver

application-dev.properties

1.#DB config
2.spring.datasource.url=jdbc:sqlserver://localhost:1433;databaseName=sample_webservice_db_dev
3.spring.datasource.username=dbuser
4.spring.datasource.password=ENC(tZTfehMYyz4EO0F0uY8fZItE7K35RtkA)
5.#spring.datasource.username=dbuser
6.#spring.datasource.password=dbuserpassword

application-pro.properties

1.#DB config
2.spring.datasource.url=jdbc:sqlserver://192.168.1.119:1433;databaseName=sample_webservice_db
3.spring.datasource.username=proUser
4.spring.datasource.password=ENC(proUserPswd)

数据库密码加密

  • 应用程序数据库密码会使用加密密钥通过__Jasypt __ 库加密。
  • 加密密钥需要添加到系统环境变量中的JASYPT_ENCRYPTOR_PASSWORD变量中。
  • 必须在属性文件中声明加密后的数据库密码。如此,系统就会了解密码需要解密,而解密则需要使用添加在系统变量中的密钥来进行。
1.spring.datasource.password=ENC(tZTfehMYyz4EO0F0uY8fZItE7K35RtkA)
  • 对于__Jasypt __加密,我们在属性文件中使用默认的加密配置,如下所示:
1.jasypt.encryptor.algorithm=PBEWithMD5AndDES
2.jasypt.encryptor.iv-generator-classname=org.jasypt.iv.NoIvGenerator
  • 我们也可以在应用的main方法中使用@EnableEncryptableProperties注解,规定数据库密码的加密配置

SampleWebservice.java

1.@SpringBootApplication
2.@EnableEncryptableProperties
3.public class SampleWebservice extends SpringBootServletInitializer {
4.--------
5.--------

JWT身份验证配置

  • 使用Spring Security实现基于JSON Web令牌的身份验证
  • 当用户登录成功时,我们会创建两个token(accessToken 和 refreshToken)并把他们返回给客户端
  • accessToken由私钥,过期时间(1小时),用户ID和角色名生成
  • refreshToken由私钥,过期时间(24小时),用户ID和角色名生成
  • 登陆成功后,每个API请求都需要在请求头Header中的Authorization键中添加accessToken
  • 在accessToken开头添加"bearer"字符串
  • 即为”bearer accessToken”
  •  accessToken将会监控每一个Web服务请求
  • 如果accessToken过期,系统会以HTTP 401状态码回复请求
  • 此时客户端需要使用refreshToken重新获取accessToken
  • 然后我们会检查refreshToken的有效性,如果没有过期则会生成一个新的accessToken和refreshToken
  • 客户端会继续使用这些新令牌
  • 如果refreshToken也过期了,就需要用户使用账号密码重新登陆了

创建令牌的过程

UnAuthorisedAccessServiceImpl.java

1.@Override
2.    public ApiSuccessResponse userLoginService(String usernameString password) {
3.        Tokens tokens = null;
4.        Users user = userService.findByUsername(username);
5.        if (user != null) {
6.            if (passwordEncryptingService.matches(password,
7.                    user.getUserAccount().getPassword())) {
8.                if (user.getUserAccount().getStatus() == Constants.ACTIVE_STATUS) {
9.                    String roleName = user.getUserAccount().getUserRole().getRoleName();
10.                    // Creating new tokens
11.                    try {
12.                        tokens = createTokens(user.getUserAccount().getId().toString(), roleName);
13.                    } catch (Exception exception) {
14.                        logger.error("Token creation failed : "exception);
15.                        throw new UnknownException();
16.                    }
17.
18.                    // Validating tokens
19.                    if (validationService.validateTokens(tokens)) {
20.                        tokens.setUserId(user.getUserAccount().getId());
21.                        return new ApiSuccessResponse(tokens);
22.
23.                    } else {
24.                        throw new UnknownException();
25.                    }
26.
27.                } else {
28.                    return new ApiSuccessResponse(new ApiResponseWithCode(Constants.USER_ACCOUNT_IS_INACTIVE_ERROR_CODE,
29.                            Constants.USER_ACCOUNT_IS_INACTIVE_ERROR_MESSAGE));
30.                }
31.
32.            } else {
33.                return new ApiSuccessResponse(new ApiResponseWithCode(Constants.USERNAME_OR_PASSWORD_IS_INCORRECT_ERROR_CODE,
34.                        Constants.USERNAME_OR_PASSWORD_IS_INCORRECT_ERROR_MESSAGE));
35.            }
36.
37.        } else {
38.            return new ApiSuccessResponse(new ApiResponseWithCode(Constants.USERNAME_OR_PASSWORD_IS_INCORRECT_ERROR_CODE,
39.                    Constants.USERNAME_OR_PASSWORD_IS_INCORRECT_ERROR_MESSAGE));
40.        }
41.    }
42.
43.    @Override
44.    public ApiSuccessResponse createNewAccessTokenUsingRefreshToken(String refreshToken) {
45.        Tokens tokens = null;
46.        UserAccounts userAccount = null;
47.        AppConfigSettings configSettings = appConfigSettingsService.findByConfigKeyAndStatus(Constants.JWT_SECRET_KEY,
48.                Constants.ACTIVE_STATUS);
49.        // Validate Refresh token
50.        userAccount = jwtTokenHandler.validate(configSettings.getConfigValue(), refreshToken);
51.        if (userAccount != null) {
52.            // Creating new tokens if provided refresh token is valid
53.            try {
54.                tokens = createTokens(userAccount.getId().toString(), userAccount.getRole());
55.            } catch (Exception exception) {
56.                logger.error("Token creation failed : "exception);
57.                throw new UnknownException();
58.            }
59.            if (validationService.validateTokens(tokens)) {
60.                tokens.setUserId(userAccount.getId());
61.                return new ApiSuccessResponse(tokens);
62.
63.            } else {
64.                throw new UnknownException();
65.            }
66.        } else {
67.            return new ApiSuccessResponse(new ApiResponseWithCode(Constants.REFRESH_TOKEN_EXPIRED_ERROR_CODE,
68.                    Constants.REFRESH_TOKEN_EXPIRED_ERROR_MESSAGE));
69.        }
70.    }
  • 上述代码中的userLoginService方法会检查用户凭据,如果有效则颁发令牌。
  • CreateNewAccessTokenUsingRefreshToken方法会在refreshToken验证成功后,生成新的accessToken和refreshToken。

过滤和验证令牌的过程

WebConfig.java

1.@Configuration
2.@EnableWebSecurity
3.@EnableGlobalMethodSecurity(prePostEnabled = true)
4.public class WebConfig extends WebSecurityConfigurerAdapter {
5.
6.    @Autowired
7.    private JwtAuthenticationProvider authenticationProvider;
8.
9.    @Autowired
10.    private JwtAuthenticationEntryPoint entryPoint;
11.
12.    @Bean
13.    public AuthenticationManager authenticationManager() {
14.        return new ProviderManager(Collections.singletonList(authenticationProvider));
15.    }
16.
17.    @Bean
18.    public JwtAuthenticationTokenFilter authenticationTokenFilter() {
19.        JwtAuthenticationTokenFilter filter = new JwtAuthenticationTokenFilter();
20.        filter.setAuthenticationManager(authenticationManager());
21.        filter.setAuthenticationSuccessHandler(new JwtSuccessHandler());
22.        return filter;
23.    }
24.
25.    @Override
26.    protected void configure(HttpSecurity httpthrows Exception {
27.        http.csrf().disable()
28.                .exceptionHandling().authenticationEntryPoint(entryPoint).and().sessionManagement()
29.                .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
30.                .addFilterBefore(new WebSecurityCorsFilter(), ChannelProcessingFilter.class)
31.                .addFilterBefore(authenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
32.                .headers().cacheControl();
33.    }
34.}
  • 这个配置将使用@EnableWebSecurity和@EnableGlobalMethodSecurity(prePostEnabled = true)两个注解来启用spring security模块
  • 这里,我们将把JWT过滤器注入到系统的HTTP请求中

JwtAuthenticationTokenFilter.java

1.public class JwtAuthenticationTokenFilter extends AbstractAuthenticationProcessingFilter {
2.
3.    private final Logger logger = LoggerFactory.getLogger(this.getClass());
4.
5.    @Autowired
6.    private GeneralServices generalServices;
7.
8.    public JwtAuthenticationTokenFilter() {
9.        super("/api/**");
10.    }
11.
12.    @Override
13.    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest,
14.                                                HttpServletResponse httpServletResponsethrows AuthenticationExceptionIOExceptionServletException {
15.                                                  -------
16.                                                  --------
17.    }
  • 在如上的类中,JwtAuthenticationTokenFilter()的方法将过滤所有URL中含有“api”命名关键字的Web服务请求
  • 所有经过过滤的Web服务请求将到达attemptAuthentication方法
  • 我们可以在这个方法里处理所有的业务逻辑

应用用户密码加密

  • 该应用中所有的用户密码都将会使用BCrypt加密

PasswordEncryptingService.java

1.public class PasswordEncryptingService {
2.
3.    public String encode(CharSequence rawPassword) {
4.        return BCrypt.hashpw(rawPassword.toString(), BCrypt.gensalt(6));
5.    }
6.
7.    public boolean matches(CharSequence rawPasswordString encodedPassword) {
8.        return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
9.    }
  • 这里,encode方法用来加密密码
  • matches方法用来交叉检查提供的密码和用户的实际密码

使用Slf4j配置日志

  • 在一个叫logback-spring.xml的文件中配置日志
  • 为了记录每个类的日志,我们需要在相应的类中注入Slf4j

示例

UserServiceImpl.java

1.@Service("UserService")
2.@Scope("prototype")
3.public class UserServiceImpl implements UserService {
4.    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
  • 上面的代码片段显示了我们如何将类注入到logger
  • 以下是记录日志的一些基本方法

logger.error("Error");

logger.info("Info");

logger.warn("Warn");

基于Swagger的API文档

  • API文档在Web服务应用程序中扮演着重要的角色
  • 之前,我们使用静态Excel文档创建API文档
  • 这个库将帮助我们在应用程序中使用注释创建API文档

Pom.xml

1.  <dependency>
2.            <groupId>io.springfox</groupId>
3.            <artifactId>springfox-boot-starter</artifactId>
4.            <version>${springfox.swagger.version}</version>
5.        </dependency>
6.
7.        <dependency>
8.            <groupId>io.springfox</groupId>
9.            <artifactId>springfox-swagger-ui</artifactId>
10.            <version>${springfox.swagger.version}</version>
11.        </dependency>
  • 为了集成Swagger,上述这些是我们要在pom文件中添加的库
  • 我们需要在应用程序中做一些配置来启用API文档

SwaggerAPIDocConfig.java

1.@Configuration
2.@EnableSwagger2
3.public class SwaggerAPIDocConfig {
4.
5.    public static final Contact DEFAULT_CONTACT = new Contact("Demo""http://www.demo.ae/",
6.            "info@demo.ae");
7.
8.    public static final ApiInfo DEFAUL_API_INFO = new ApiInfo("Sample Application",
9.            "Sample Application description.",
10.            "1.0.0",
11.            "http://www.sampleapplication.ae/",
12.            DEFAULT_CONTACT"Open licence",
13.            "http://www.sampleapplication.ae/#license",
14.            new ArrayList<VendorExtension>());
15.
16.    private static final Set<String> DEFAULT_PRODICERS_AND_CONSUMERS =
17.            new HashSet<>(Arrays.asList("application/json""application/xml"));
18.
19.    @Bean
20.    public Docket api() {
21.        return new Docket(DocumentationType.SWAGGER_2)
22.                .apiInfo(DEFAUL_API_INFO)
23.                .produces(DEFAULT_PRODICERS_AND_CONSUMERS)
24.                .consumes(DEFAULT_PRODICERS_AND_CONSUMERS)
25.                .select()
26.                .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
27.                .paths(PathSelectors.any())
28.                .build();
29.    }
30.}
  • 正如在上边的类中看到的,我们需要添加关于项目的基本信息
  • 我们需要告诉Swagger从哪个类创建API文档,这是在.apis(RequestHandlerSelectors.withClassAnnotation,(RestController.class))命名行下配置的
  • 我们可以通过http://localhost:8080/sampleWebService/apidoc来访问Swagger的API文档

Postman脚本

  • 我们可以在代码库中找到2个Postman JSON脚本,然后将它们导入到Postman客户端
  • 首先执行登陆的Web服务请求,然后执行其余web服务请求

原文链接:How To Build Web Service Using Spring Boot 2.x,作者:Vishnu Viswambharan

责任编辑:华轩 来源: 51CTO
相关推荐

2021-03-04 10:11:50

MongoDBSpring BootSpring Boot

2020-08-19 17:56:46

缓存Redis集中式

2021-02-03 10:49:34

JTA分布式事务

2021-12-31 08:48:23

Logback日志管理

2018-05-04 15:27:22

Spring Boo Web开发

2022-10-10 08:00:00

微服务Spring Boo容器

2020-06-04 17:38:49

PythonFastAPIWeb服务

2018-07-09 09:27:10

Spring Clou微服务架构

2022-02-22 08:30:12

Husky代码工作流

2024-04-02 08:17:40

2017-06-08 11:00:09

HDFSHadoopYARN

2017-03-23 09:29:06

2024-04-18 09:34:28

Reactor项目异步编程

2017-08-02 14:44:06

Spring Boot开发注解

2022-09-05 08:00:00

Java微服务AuraDB

2020-02-17 16:28:49

开发技能代码

2022-03-04 15:19:59

Spring BooJavaVert.x

2022-04-15 08:25:00

SpringjQuery客户端

2023-04-19 07:39:55

RustHTTP服务器

2020-06-30 07:58:39

微服务Spring BootCloud
点赞
收藏

51CTO技术栈公众号