想用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.@RestController3.@Validated4.public class UserController {5.6. private static final Logger logger = LoggerFactory.getLogger(UserController.class);7.8. @Autowired9. private GeneralServices generalServices;10.11. @Autowired12. private UserService userService;13.14. /**15. * Web service to create new user16. *17. * @param httpServletRequest18. * @param user19. * @return20. */21. @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = 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(user, generalServices.getApiRequestedUserId(httpServletRequest));26. logger.debug("<--- Service to save new user response : given --->");27. return ResponseEntity.status(HttpStatus.CREATED).body(apiResponse);28.29. }30.31.}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.
@RequestMapping("/api/v1/user")注解用来声明Web服务的类别
@RestController注解配置该类来接收Restful的 Web服务调用
@PostMapping()注解决定了HTTP请求类型
consume和consume标记来确定HTTP请求和响应的内容类型
通过controller层,API请求将被带到服务层。所有业务逻辑都将在这里处理,然后它将使用JPA与数据库通信。
通用错误处理
每当异常发生时,它将从相应的类抛出,并在CommonExceptionHandlingController中处理。我们必须分别处理每种异常类型。这个功能是在ControllerAdvice注解的帮助下执行的。
示例
1.@ControllerAdvice2.public class CommonExceptionHandlingController extends ResponseEntityExceptionHandler {3.4. private static final Logger logger = 5. LoggerFactory.getLogger(CommonExceptionHandlingController.class);6.7. @Override8. protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException httpRequestMethodNotSupportedException,9. HttpHeaders headers, HttpStatus status, WebRequest request) {10. return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiErrorResponse(Constants.WRONG_HTTP_METHOD,11. Constants.WRONG_HTTP_METHOD_ERROR_MESSAGE, Calendar.getInstance().getTimeInMillis()));12. }13.14. @Override15. protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException methodArgumentNotValidException,16. HttpHeaders headers, HttpStatus status, WebRequest 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_MESSAGE, Calendar.getInstance().getTimeInMillis()));19. }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.
Spring Data(JPA)配置
应用程序与数据库的所有交互都将由JPA处理
JPA将为应用程序中的所有逻辑对象提供一个Entity类和一个相应的Repository接口
Entity类
1.@Entity2.@Table(name = "tbl_users")3.public class Users implements Serializable {4. private static final long serialVersionUID = 1L;5.6. @Id7. @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;1.2.3.4.5.6.7.8.9.10.11.12.13.
Repository接口
1.public interface UserRepository extends JpaRepository<Users, Long> {2.3. /**4. * To find user object using username5. *6. * @param username7. * @return8. */9. Users findByUserAccountUsername(String username);1.2.3.4.5.6.7.8.9.
其他的JPA配置将会在application.properties文件中完成
在application.properties中的JPA数据库配置
1.spring.jpa.show-sql=false2.spring.jpa.hibernate.dialect=org.hibernate.dialect.SQLServer2012Dialect3.spring.jpa.hibernate.ddl-auto = update4.5.spring.jpa.properties.hibernate.show_sql=false6.spring.jpa.properties.hibernate.format_sql=false7.spring.jpa.properties.hibernate.use_sql=true8.spring.jpa.open-in-view=false9.spring.jpa.properties.hibernate.hbm2ddl.auto=update10.spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl11.spring.jpa.hibernate.connection.provider_class=org.hibernate.hikaricp.internal.HikariCPConnectionProvider1.2.3.4.5.6.7.8.9.10.11.
数据库配置
数据库名称写在application.properties文件中
其他信息(比如URL连接地址和账号密码)将写到另外两个不同属性文件中
application-dev.properties
此处写开发环境的配置信息
application-pro.properties
此处写生产环境的配置信息
spring.profiles.active=dev1.
上述提到的属性配置写到application.properties文件中
它将决定系统使用哪个子配置文件(开发环境还是生产环境)
application.properties
1.#DB config2.spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver1.2.
application-dev.properties
1.#DB config2.spring.datasource.url=jdbc:sqlserver://localhost:1433;databaseName=sample_webservice_db_dev3.spring.datasource.username=dbuser4.spring.datasource.password=ENC(tZTfehMYyz4EO0F0uY8fZItE7K35RtkA)5.#spring.datasource.username=dbuser6.#spring.datasource.password=dbuserpassword1.2.3.4.5.6.
application-pro.properties
1.#DB config2.spring.datasource.url=jdbc:sqlserver://192.168.1.119:1433;databaseName=sample_webservice_db3.spring.datasource.username=proUser4.spring.datasource.password=ENC(proUserPswd)1.2.3.4.
数据库密码加密
应用程序数据库密码会使用加密密钥通过__Jasypt __ 库加密。
加密密钥需要添加到系统环境变量中的JASYPT_ENCRYPTOR_PASSWORD变量中。
必须在属性文件中声明加密后的数据库密码。如此,系统就会了解密码需要解密,而解密则需要使用添加在系统变量中的密钥来进行。
1.spring.datasource.password=ENC(tZTfehMYyz4EO0F0uY8fZItE7K35RtkA)1.
对于__Jasypt __加密,我们在属性文件中使用默认的加密配置,如下所示:
1.jasypt.encryptor.algorithm=PBEWithMD5AndDES2.jasypt.encryptor.iv-generator-classname=org.jasypt.iv.NoIvGenerator1.2.
我们也可以在应用的main方法中使用@EnableEncryptableProperties注解,规定数据库密码的加密配置
SampleWebservice.java
1.@SpringBootApplication2.@EnableEncryptableProperties3.public class SampleWebservice extends SpringBootServletInitializer {4.--------5.--------1.2.3.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.@Override2. public ApiSuccessResponse userLoginService(String username, String 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 tokens11. 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 tokens19. 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. @Override44. 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 token50. userAccount = jwtTokenHandler.validate(configSettings.getConfigValue(), refreshToken);51. if (userAccount != null) {52. // Creating new tokens if provided refresh token is valid53. 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. }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.
上述代码中的userLoginService方法会检查用户凭据,如果有效则颁发令牌。
CreateNewAccessTokenUsingRefreshToken方法会在refreshToken验证成功后,生成新的accessToken和refreshToken。
过滤和验证令牌的过程
WebConfig.java
1.@Configuration2.@EnableWebSecurity3.@EnableGlobalMethodSecurity(prePostEnabled = true)4.public class WebConfig extends WebSecurityConfigurerAdapter {5.6. @Autowired7. private JwtAuthenticationProvider authenticationProvider;8.9. @Autowired10. private JwtAuthenticationEntryPoint entryPoint;11.12. @Bean13. public AuthenticationManager authenticationManager() {14. return new ProviderManager(Collections.singletonList(authenticationProvider));15. }16.17. @Bean18. public JwtAuthenticationTokenFilter authenticationTokenFilter() {19. JwtAuthenticationTokenFilter filter = new JwtAuthenticationTokenFilter();20. filter.setAuthenticationManager(authenticationManager());21. filter.setAuthenticationSuccessHandler(new JwtSuccessHandler());22. return filter;23. }24.25. @Override26. protected void configure(HttpSecurity http) throws 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.}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.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. @Autowired6. private GeneralServices generalServices;7.8. public JwtAuthenticationTokenFilter() {9. super("/api/**");10. }11.12. @Override13. public Authentication attemptAuthentication(HttpServletRequest httpServletRequest,14. HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {15. -------16. --------17. }1.2.3.4.5.6.7.8.9.10.11.12.13.14.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 rawPassword, String encodedPassword) {8. return BCrypt.checkpw(rawPassword.toString(), encodedPassword);9. }1.2.3.4.5.6.7.8.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);1.2.3.4.
上面的代码片段显示了我们如何将类注入到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>1.2.3.4.5.6.7.8.9.10.11.
为了集成Swagger,上述这些是我们要在pom文件中添加的库
我们需要在应用程序中做一些配置来启用API文档
SwaggerAPIDocConfig.java
1.@Configuration2.@EnableSwagger23.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. @Bean20. 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.}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.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