Защита Spring Boot RESTful Service используя Auth0 JWT
1. Restful Spring Boot & JWT
Допустим у вас есть RESTful API написанный на Spring Boot, Client (другое приложение) может вызвать ваш RESTful API и получить результат.
Но не все RESTful API могут быть публичны, из-за его чувствительности, поэтому вам нужно их защитить. Есть некоторые техники, для защиты вашего RESTful API:
- Защита RESTful API с Basic Authentication (Базовая аутентификация).
- Защита RESTful API с JWT (JSON Web Token).
Есть некоготые другие техники, которые я не перечислил выше. Но в основном Client хочет вызвать защищенный RESTful API, ему нужно отправить запрос с прикрепленной информацией аутентификации (Возможно username/password).
Смотрите так же:
- Tìm hiểu về JWT (JSON Web Token)
Вызов Client-а публичный (public) REST API это очень просто, похоже на изображение ниже:
В случае REST API защищен с помощью Basic Authentication, Client должен закодировать строку "username:password" с алгоритмом Base64 чтобы получить массив byte, прикрепить данный массив на Request Headers в каждом вызове REST API.
С REST API защищенные с Auth0, вызов будет немного сложнее.
- Шаг 1: Вы должны отправить request (запрос) логина, который содержит username/password, и получить ответ "Authorization String" (Строка авторизации) прикрепленная на Response Header.
- Шаг 2: После получения "Authorization String", прикрепить ее на Request Header для вызова REST API.
2. Создать проект Spring Boot
На Eclipse создать проект Spring Boot.
Объявить библиотеки Auth0 чтобы использовать в данном проекте.
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/auth0 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0</artifactId>
<version>1.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/auth0-spring-security-api -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0-spring-security-api</artifactId>
<version>1.0.0</version>
</dependency>
Полное содержание файла pom.xml:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>SpringBootJWT</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SpringBootJWT</name>
<description>Spring Boot + Rest + JWT</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/auth0 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0</artifactId>
<version>1.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/auth0-spring-security-api -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0-spring-security-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
SpringBootJwtApplication.java
package org.o7planning.sbjwt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootJwtApplication.class, args);
}
}
3. Model, DAO & REST API
Employee.java
package org.o7planning.sbjwt.model;
public class Employee {
private String empNo;
private String empName;
private String position;
public Employee() {
}
public Employee(String empNo, String empName, String position) {
this.empNo = empNo;
this.empName = empName;
this.position = position;
}
public String getEmpNo() {
return empNo;
}
public void setEmpNo(String empNo) {
this.empNo = empNo;
}
public String getEmpName() {
return empName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
}
EmployeeDAO.java
package org.o7planning.sbjwt.dao;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.o7planning.sbjwt.model.Employee;
import org.springframework.stereotype.Repository;
@Repository
public class EmployeeDAO {
private static final Map<String, Employee> empMap = new HashMap<String, Employee>();
static {
initEmps();
}
private static void initEmps() {
Employee emp1 = new Employee("E01", "Smith", "Clerk");
Employee emp2 = new Employee("E02", "Allen", "Salesman");
Employee emp3 = new Employee("E03", "Jones", "Manager");
empMap.put(emp1.getEmpNo(), emp1);
empMap.put(emp2.getEmpNo(), emp2);
empMap.put(emp3.getEmpNo(), emp3);
}
public Employee getEmployee(String empNo) {
return empMap.get(empNo);
}
public Employee addEmployee(Employee emp) {
empMap.put(emp.getEmpNo(), emp);
return emp;
}
public Employee updateEmployee(Employee emp) {
empMap.put(emp.getEmpNo(), emp);
return emp;
}
public void deleteEmployee(String empNo) {
empMap.remove(empNo);
}
public List<Employee> getAllEmployees() {
Collection<Employee> c = empMap.values();
List<Employee> list = new ArrayList<Employee>();
list.addAll(c);
return list;
}
}
MainRESTController.java
package org.o7planning.sbjwt.controller;
import java.util.List;
import org.o7planning.sbjwt.dao.EmployeeDAO;
import org.o7planning.sbjwt.model.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MainRESTController {
@Autowired
private EmployeeDAO employeeDAO;
@RequestMapping("/")
@ResponseBody
public String welcome() {
return "Welcome to Spring Boot + REST + JWT Example.";
}
@RequestMapping("/test")
@ResponseBody
public String test() {
return "{greeting: 'Hello'}";
}
// URL:
// http://localhost:8080/employees
@RequestMapping(value = "/employees", //
method = RequestMethod.GET, //
produces = { MediaType.APPLICATION_JSON_VALUE, //
MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public List<Employee> getEmployees() {
List<Employee> list = employeeDAO.getAllEmployees();
return list;
}
// URL:
// http://localhost:8080/employee/{empNo}
@RequestMapping(value = "/employee/{empNo}", //
method = RequestMethod.GET, //
produces = { MediaType.APPLICATION_JSON_VALUE, //
MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public Employee getEmployee(@PathVariable("empNo") String empNo) {
return employeeDAO.getEmployee(empNo);
}
// URL:
// http://localhost:8080/employee
@RequestMapping(value = "/employee", //
method = RequestMethod.POST, //
produces = { MediaType.APPLICATION_JSON_VALUE, //
MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public Employee addEmployee(@RequestBody Employee emp) {
System.out.println("(Service Side) Creating employee: " + emp.getEmpNo());
return employeeDAO.addEmployee(emp);
}
// URL:
// http://localhost:8080/employee
@RequestMapping(value = "/employee", //
method = RequestMethod.PUT, //
produces = { MediaType.APPLICATION_JSON_VALUE, //
MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public Employee updateEmployee(@RequestBody Employee emp) {
System.out.println("(Service Side) Editing employee: " + emp.getEmpNo());
return employeeDAO.updateEmployee(emp);
}
// URL:
// http://localhost:8080/employee/{empNo}
@RequestMapping(value = "/employee/{empNo}", //
method = RequestMethod.DELETE, //
produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public void deleteEmployee(@PathVariable("empNo") String empNo) {
System.out.println("(Service Side) Deleting employee: " + empNo);
employeeDAO.deleteEmployee(empNo);
}
}
4. Security & Login Filter
Данное приложение имеет функцию логина, Client может отправить запрос логина с методом POST. Поэтому не нужно создавать страницу логина. Вместо этого мы имеем Filter (фильтр), когда request (запрос) с ссылкой /login, будет обработан данным Filter (фильтром).
Request (запросы) которые хотят пойти к Controller, должны пройти Filter (фильтр):
В данной статье, чтобы было проще я создам 2 User в памяти, Client может войти с одним из 2 следующих User:
- tom/123
- jerry/123
WebSecurityConfig.java
package org.o7planning.sbjwt.config;
import org.o7planning.sbjwt.filter.JWTAuthenticationFilter;
import org.o7planning.sbjwt.filter.JWTLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
// No need authentication.
.antMatchers("/").permitAll() //
.antMatchers(HttpMethod.POST, "/login").permitAll() //
.antMatchers(HttpMethod.GET, "/login").permitAll() // For Test on Browser
// Need authentication.
.anyRequest().authenticated()
//
.and()
//
// Add Filter 1 - JWTLoginFilter
//
.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
//
// Add Filter 2 - JWTAuthenticationFilter
//
.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String password = "123";
String encrytedPassword = this.passwordEncoder().encode(password);
System.out.println("Encoded password of 123=" + encrytedPassword);
InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> //
mngConfig = auth.inMemoryAuthentication();
// Defines 2 users, stored in memory.
// ** Spring BOOT >= 2.x (Spring Security 5.x)
// Spring auto add ROLE_
UserDetails u1 = User.withUsername("tom").password(encrytedPassword).roles("USER").build();
UserDetails u2 = User.withUsername("jerry").password(encrytedPassword).roles("USER").build();
mngConfig.withUser(u1);
mngConfig.withUser(u2);
// If Spring BOOT < 2.x (Spring Security 4.x)):
// Spring auto add ROLE_
// mngConfig.withUser("tom").password("123").roles("USER");
// mngConfig.withUser("jerry").password("123").roles("USER");
}
}
Когда request (запрос) с ссылкой /login отправляется к Server, он будет обработан JWTLoginFilter, данный класс проверит username/password, при действительности, строка авторизации (Authorization string) будет прикреплена к Response Header чтобы вернуть к Client.
JWTLoginFilter.java
package org.o7planning.sbjwt.filter;
import java.io.IOException;
import java.util.Collections;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.o7planning.sbjwt.service.TokenAuthenticationService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
public JWTLoginFilter(String url, AuthenticationManager authManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.printf("JWTLoginFilter.attemptAuthentication: username/password= %s,%s", username, password);
System.out.println();
return getAuthenticationManager()
.authenticate(new UsernamePasswordAuthenticationToken(username, password, Collections.emptyList()));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
System.out.println("JWTLoginFilter.successfulAuthentication:");
// Write Authorization to Headers of Response.
TokenAuthenticationService.addAuthentication(response, authResult.getName());
String authorizationString = response.getHeader("Authorization");
System.out.println("Authorization String=" + authorizationString);
}
}
Класс TokenAuthenticationService является утилитарным классом, он записывает "Authorization string" (Строку авторизации) в Response Header чтобы вернуть к Client. Данная строка авторизации работает в течении определенного времени (10 дней). Это означает Client нужно войти только один раз и получить "Строку авторизации" и можно использовать в течении вышеупомянутого времени. Когда "Строка авторизации" вышел срок, Client должен войти еще раз чтобы получить новую строку авторизации.
TokenAuthenticationService.java
package org.o7planning.sbjwt.service;
import java.util.Collections;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
public class TokenAuthenticationService {
static final long EXPIRATIONTIME = 864_000_000; // 10 days
static final String SECRET = "ThisIsASecret";
static final String TOKEN_PREFIX = "Bearer";
static final String HEADER_STRING = "Authorization";
public static void addAuthentication(HttpServletResponse res, String username) {
String JWT = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
.signWith(SignatureAlgorithm.HS512, SECRET).compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
}
public static Authentication getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody()
.getSubject();
return user != null ? new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList()) : null;
}
return null;
}
}
Чтобы вызвать REST API, request (запросы) будут прикреплять "Authorization string" на Request Header. Класс JWTAuthenticationFilter проверит "Authorization string", при действительности request будет аутентифицирован, может пройти дальше к Controller.
JWTAuthenticationFilter.java
package org.o7planning.sbjwt.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.o7planning.sbjwt.service.TokenAuthenticationService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
public class JWTAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
System.out.println("JWTAuthenticationFilter.doFilter");
Authentication authentication = TokenAuthenticationService
.getAuthentication((HttpServletRequest) servletRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(servletRequest, servletResponse);
}
}
5. Протестировать приложение с браузером
Вы можете использовать браузер для теста функции Login, и посмотреть работу класса JWTLoginFilter. OK!, Пройдите по следующей ссылке в браузере:
Смотреть информацию записанную на окне Console у Eclipse:
JWTLoginFilter.attemptAuthentication: username/password= tom,123
JWTLoginFilter.successfulAuthentication:
Authorization String=Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b20iLCJleHAiOjE1MjA3OTQyNjF9.GP0p5Aaj0HNMShNEAPbieHPeYxeGJ_-lB8ahHr6dJvrs_pAoSgGNCj8bNzRYpi4H7cJ1xQ_DZwV1bMw6ihK2Mw
JWTAuthenticationFilter.doFilter
JWTAuthenticationFilter.doFilter
6. Протестировать приложение с RestTemplate
Создать пример используя класс RestTemplate (Spring REST Client) вызвать REST API защищенный с помощью Auth0:
JWTClientExample.java
package org.o7planning.sbjwt.restclient;
import java.util.Arrays;
import java.util.List;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
public class JWTClientExample {
static final String URL_LOGIN = "http://localhost:8080/login";
static final String URL_EMPLOYEES = "http://localhost:8080/employees";
// POST Login
// @return "Authorization string".
private static String postLogin(String username, String password) {
// Request Header
HttpHeaders headers = new HttpHeaders();
// Request Body
MultiValueMap<String, String> parametersMap = new LinkedMultiValueMap<String, String>();
parametersMap.add("username", username);
parametersMap.add("password", password);
// Request Entity
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parametersMap, headers);
// RestTemplate
RestTemplate restTemplate = new RestTemplate();
// POST Login
ResponseEntity<String> response = restTemplate.exchange(URL_LOGIN, //
HttpMethod.POST, requestEntity, String.class);
HttpHeaders responseHeaders = response.getHeaders();
List<String> list = responseHeaders.get("Authorization");
return list == null || list.isEmpty() ? null : list.get(0);
}
private static void callRESTApi(String restUrl, String authorizationString) {
// HttpHeaders
HttpHeaders headers = new HttpHeaders();
//
// Authorization string (JWT)
//
headers.set("Authorization", authorizationString);
//
headers.setAccept(Arrays.asList(new MediaType[] { MediaType.APPLICATION_JSON }));
// Request to return JSON format
headers.setContentType(MediaType.APPLICATION_JSON);
// HttpEntity<String>: To get result as String.
HttpEntity<String> entity = new HttpEntity<String>(headers);
// RestTemplate
RestTemplate restTemplate = new RestTemplate();
// Send request with GET method, and Headers.
ResponseEntity<String> response = restTemplate.exchange(URL_EMPLOYEES, //
HttpMethod.GET, entity, String.class);
String result = response.getBody();
System.out.println(result);
}
public static void main(String[] args) {
String username = "tom";
String password = "123";
String authorizationString = postLogin(username, password);
System.out.println("Authorization String=" + authorizationString);
// Call REST API:
callRESTApi(URL_EMPLOYEES, authorizationString);
}
}
Смотреть так же:
Руководства Spring Boot
- Установите Spring Tool Suite для Eclipse
- Руководство Spring для начинающих
- Руководство Spring Boot для начинающих
- Общие свойства Spring Boot
- Руководство Spring Boot и Thymeleaf
- Руководство Spring Boot и FreeMarker
- Руководство Spring Boot и Groovy
- Руководство Spring Boot и Mustache
- Руководство Spring Boot и JSP
- Руководство Spring Boot, Apache Tiles, JSP
- Используйте Logging в Spring Boot
- Мониторинг приложений с помощью Spring Boot Actuator
- Создание веб-приложения с несколькими языками с помощью Spring Boot
- Используйте несколько ViewResolver в Spring Boot
- Используйте Twitter Bootstrap в Spring Boot
- Руководство Spring Boot Interceptor
- Руководство Spring Boot, Spring JDBC и Spring Transaction
- Руководство Spring JDBC
- Руководство Spring Boot, JPA и Spring Transaction
- Руководство Spring Boot и Spring Data JPA
- Руководство Spring Boot, Hibernate и Spring Transaction
- Интеграция Spring Boot, JPA и H2 Database
- Руководство Spring Boot и MongoDB
- Используйте несколько DataSources с Spring Boot и JPA
- Используйте несколько DataSource с Spring Boot и RoutingDataSource
- Создайте приложение для входа с Spring Boot, Spring Security, Spring JDBC
- Создайте приложение для входа с Spring Boot, Spring Security, JPA
- Создайте приложение регистрации пользователей с помощью Spring Boot, Spring Form Validation
- Пример OAuth2 Social Login в Spring Boot.
- Запускать фоновые запланированные задачи в Spring
- Пример CRUD Restful Web Service c Spring Boot
- Пример Spring Boot Restful Client c RestTemplate
- Пример CRUD с Spring Boot, REST и AngularJS
- Защита Spring Boot RESTful Service используя Basic Authentication
- Защита Spring Boot RESTful Service используя Auth0 JWT
- Пример Upload file c Spring Boot
- Пример Download file c Spring Boot
- Пример Upload file c Spring Boot и jQuery Ajax
- Пример Upload file c Spring Boot и AngularJS
- Создание веб-приложения для корзины покупок с помощью Spring Boot, Hibernate
- Руководство Spring Email
- Создайте простое приложение Chat с Spring Boot и Websocket
- Разверните приложение Spring Boot на Tomcat Server
- Развертывание приложения Spring Boot на Oracle WebLogic Server
- Установите бесплатный сертификат Let's Encrypt SSL для Spring Boot
- Настройте Spring Boot для перенаправления HTTP на HTTPS
Show More