Compare commits
2 Commits
with-http-
...
with-http-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdcfbc4a1d | ||
|
|
94ce6a6424 |
144
README.md
Normal file
144
README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Spring Security REST API Boilerplate
|
||||
___
|
||||
## Overview
|
||||
|
||||
This is a boilerplate Spring Boot project designed to kickstart the development of RESTful APIs with built-in Spring Security for authentication and authorization.
|
||||
|
||||
## Features
|
||||
|
||||
- **Spring Boot:** Utilize the power of the Spring Boot framework for building robust and scalable applications.
|
||||
- **Spring Security:** Implement secure authentication and authorization mechanisms to protect your REST API.
|
||||
- **RESTful API:** Design and develop a clean and efficient RESTful API to handle your application's business logic.
|
||||
- **Customizable:** Easily extend and customize the project to fit your specific requirements.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Clone the repository: `git clone https://github.com/hakimfauzi23/boilerplate-spring-security.git`
|
||||
2. Navigate to the project directory `cd boilerplate-spring-security`
|
||||
3. Configure `src/main/resources/application.properties`
|
||||
```properties
|
||||
spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false
|
||||
spring.datasource.username= root
|
||||
spring.datasource.password=
|
||||
spring.jpa.hibernate.ddl-auto= update
|
||||
|
||||
# App Properties
|
||||
spring.app.jwtSecret= ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
spring.app.jwtExpirationMs= 60000
|
||||
spring.app.jwtRefreshExpirationMs= 259200000
|
||||
```
|
||||
4. Create a new database that has a same name in `spring.datasource.url` properties
|
||||
5. Build the application JAR File : `mvn clean package`
|
||||
6. Run the application by running the JAR File : `java -jar target/boilerplate-spring-security-0.0.1-SNAPSHOT.jar`
|
||||
7. Application is running, now you can experiment on the Authentication & Authorization in this project!
|
||||
|
||||
## Authentication Feature
|
||||
The Authentication feature involves generating a JSON Web Token (JWT) for inclusion in the header of each API request. The feature encompasses three distinct endpoints: `api/auth/signup`, `api/auth/signin`, and `api/auth/refresh-token`. Below is a breakdown of each API endpoint:
|
||||
___
|
||||
### Sign Up Endpoint
|
||||
This is for create new user credentials so the authentication login can be done with the user credential.
|
||||
|
||||
**API Endpoint:** `http://localhost:8080/api/auth/signup`
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"username":"user1",
|
||||
"email":"user1@mail.com",
|
||||
"password":"12345678",
|
||||
"role": ["user"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "User registered successfully!"
|
||||
}
|
||||
```
|
||||
___
|
||||
### Sign In Endpoint
|
||||
This process involves generating an Access Token (JWT) by sending a request to the sign-in endpoint with the user credentials previously created through the sign-up endpoint.
|
||||
|
||||
**API Endpoint:** `http://localhost:8080/api/auth/signin`
|
||||
|
||||
**Request**
|
||||
```json
|
||||
{
|
||||
"username" : "user1",
|
||||
"password" : "12345678"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**
|
||||
```json
|
||||
{
|
||||
"refreshToken": "80b5f84f-c812-4efb-90a8-94893ec460a9",
|
||||
"id": 2,
|
||||
"username": "user1",
|
||||
"email": "user1@mail.com",
|
||||
"roles": [
|
||||
"ROLE_USER"
|
||||
],
|
||||
"tokenType": "Bearer",
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOi........"
|
||||
}
|
||||
```
|
||||
### Refresh Token Endpoint
|
||||
The access token comes with an expiration time. In situations where the access token has expired, but the refresh token is still valid, the refresh token can be employed to generate a new access token.
|
||||
|
||||
**API Endpoint:** `http://localhost:8080/api/auth/signin`
|
||||
|
||||
|
||||
**Request**
|
||||
```json
|
||||
{
|
||||
"refreshToken" : "6c276542-4fdf-4d7c-ba2d-dbd42cc3cfe9"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**
|
||||
```json
|
||||
{
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIs............",
|
||||
"refreshToken": "80b5f84f-c812-4efb-90a8-94893ec460a9",
|
||||
"tokenType": "Bearer"
|
||||
}
|
||||
```
|
||||
|
||||
___
|
||||
## Authorization Feature
|
||||
After gaining an understanding of Authentication and learning how to generate the Access Token, the next step is to comprehend the Authorization feature. This feature is employed to filter the JWT Token based on whether it possesses a role that grants access to a specific endpoint. If the Authorization feature determines that the token lacks the requisite role, access will be denied.
|
||||
|
||||
Here's how to make the endpoint have the authorization, please use `@PreAuthorize("hasRole('__ROLE NAME__')")` annotation like below:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/test")
|
||||
public class TestController {
|
||||
|
||||
@GetMapping("/all")
|
||||
public String allAccess() {
|
||||
return "Public Content.";
|
||||
}
|
||||
|
||||
@GetMapping("/user")
|
||||
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
|
||||
public String userAccess() {
|
||||
return "User Content.";
|
||||
}
|
||||
|
||||
@GetMapping("/admin")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public String adminAccess() {
|
||||
return "Admin Board.";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is how to use Access Token that generated in Authentication Feature, you can use the JWT token in the header `Authorization` with starts of `Bearer` then your Access Token.
|
||||
|
||||
```shell
|
||||
curl -X GET http://localhost:8080/api/test/user \
|
||||
-H "Authorization: Bearer __ACCESS TOKEN__" \
|
||||
-H "Other-Header: Header-Value"
|
||||
```
|
||||
@@ -6,19 +6,18 @@ import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.Role;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.request.LoginRequest;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.request.SignupRequest;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.request.TokenRefreshRequest;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response.JwtResponse;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response.MessageResponse;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response.UserInfoResponse;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response.TokenRefreshResponse;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.JwtUtils;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception.TokenRefreshException;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.RoleRepository;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.UserRepository;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.RefreshTokenService;
|
||||
import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsImpl;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
@@ -30,7 +29,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -106,61 +104,37 @@ public class AuthController {
|
||||
.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
String jwt = jwtUtils.generateJwtToken(authentication);
|
||||
|
||||
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
|
||||
|
||||
ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(userDetails);
|
||||
|
||||
List<String> roles = userDetails.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId());
|
||||
|
||||
ResponseCookie jwtRefreshCookie = jwtUtils.generateRefreshJwtCookie(refreshToken.getToken());
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
|
||||
.header(HttpHeaders.SET_COOKIE, jwtRefreshCookie.toString())
|
||||
.body(new UserInfoResponse(userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), roles));
|
||||
}
|
||||
|
||||
@PostMapping("/signout")
|
||||
public ResponseEntity<?> logoutUser() {
|
||||
Object principle = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
if (!Objects.equals(principle.toString(), "anonymousUser")) {
|
||||
Long userId = ((UserDetailsImpl) principle).getId();
|
||||
refreshTokenService.deleteByUserId(userId);
|
||||
}
|
||||
|
||||
ResponseCookie jwtCookie = jwtUtils.getCleanJwtCookie();
|
||||
ResponseCookie jwtRefreshCookie = jwtUtils.getCleanJwtRefreshCookie();
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
|
||||
.header(HttpHeaders.SET_COOKIE, jwtRefreshCookie.toString())
|
||||
.body(new MessageResponse("You've been signed out!"));
|
||||
return ResponseEntity.ok(new JwtResponse(
|
||||
jwt,
|
||||
refreshToken.getToken(),
|
||||
userDetails.getId(),
|
||||
userDetails.getUsername(),
|
||||
userDetails.getEmail(),
|
||||
roles));
|
||||
}
|
||||
|
||||
@PostMapping("/refresh-token")
|
||||
public ResponseEntity<?> refreshToken(HttpServletRequest request) {
|
||||
String refreshToken = jwtUtils.getJwtRefreshFromCookies(request);
|
||||
public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) {
|
||||
String requestRefreshToken = request.getRefreshToken();
|
||||
|
||||
if ((refreshToken != null) && (refreshToken.length() > 0)) {
|
||||
return refreshTokenService.findByToken(refreshToken)
|
||||
.map(refreshTokenService::verifyExpiration)
|
||||
.map(RefreshToken::getUser)
|
||||
.map(user -> {
|
||||
ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(user);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
|
||||
.body(new MessageResponse("Token is refreshed successfully!"));
|
||||
})
|
||||
.orElseThrow(() -> new TokenRefreshException(refreshToken,
|
||||
"Refresh token is not in database!"));
|
||||
}
|
||||
|
||||
return ResponseEntity.badRequest().body(new MessageResponse("Refresh Token is empty!"));
|
||||
return refreshTokenService.findByToken(requestRefreshToken)
|
||||
.map(refreshTokenService::verifyExpiration)
|
||||
.map(RefreshToken::getUser)
|
||||
.map(user -> {
|
||||
String token = jwtUtils.generateTokenFromUsername(user.getUsername());
|
||||
return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken));
|
||||
})
|
||||
.orElseThrow(() -> new TokenRefreshException(requestRefreshToken,
|
||||
"Refresh token is not in database!"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class TokenRefreshRequest {
|
||||
|
||||
@NotBlank
|
||||
private String refreshToken;
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
public void setRefreshToken(String refreshToken) {
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,19 +2,49 @@ package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.res
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class UserInfoResponse {
|
||||
public class JwtResponse {
|
||||
|
||||
private String token;
|
||||
private String type = "Bearer";
|
||||
private String refreshToken;
|
||||
private Long id;
|
||||
private String username;
|
||||
private String email;
|
||||
private List<String> roles;
|
||||
|
||||
public UserInfoResponse(Long id, String username, String email, List<String> roles) {
|
||||
public JwtResponse(String token, String refreshToken, Long id, String username, String email, List<String> roles) {
|
||||
this.token = token;
|
||||
this.refreshToken = refreshToken;
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.token = accessToken;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
public void setRefreshToken(String refreshToken) {
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getTokenType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setTokenType(String tokenType) {
|
||||
this.type = tokenType;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response;
|
||||
|
||||
public class TokenRefreshResponse {
|
||||
|
||||
private String accessToken;
|
||||
private String refreshToken;
|
||||
private String tokenType = "Bearer";
|
||||
|
||||
public TokenRefreshResponse(String accessToken, String refreshToken) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
public void setRefreshToken(String refreshToken) {
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getTokenType() {
|
||||
return tokenType;
|
||||
}
|
||||
|
||||
public void setTokenType(String tokenType) {
|
||||
this.tokenType = tokenType;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -49,7 +50,12 @@ public class AuthTokenFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
private String parseJwt(HttpServletRequest request) {
|
||||
String jwt = jwtUtils.getJwtFromCookies(request);
|
||||
return jwt;
|
||||
String headerAuth = request.getHeader("Authorization");
|
||||
|
||||
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
|
||||
return headerAuth.substring(7);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
@@ -28,52 +29,31 @@ public class JwtUtils {
|
||||
@Value("${spring.app.jwtExpirationMs}")
|
||||
private int jwtExpirationMs;
|
||||
|
||||
@Value("${spring.app.jwtCookieName}")
|
||||
private String jwtCookie;
|
||||
public String generateJwtToken(Authentication authentication) {
|
||||
|
||||
@Value("${spring.app.jwtRefreshCookieName}")
|
||||
private String jwtRefreshCookie;
|
||||
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
|
||||
return generateTokenFromUsername(userPrincipal.getUsername());
|
||||
|
||||
public String getJwtFromCookies(HttpServletRequest request) {
|
||||
return getCookieValueByName(request, jwtCookie);
|
||||
}
|
||||
|
||||
public String getJwtRefreshFromCookies(HttpServletRequest request) {
|
||||
return getCookieValueByName(request, jwtRefreshCookie);
|
||||
}
|
||||
|
||||
public ResponseCookie getCleanJwtCookie() {
|
||||
return ResponseCookie.from(jwtCookie, null).path("/api").build();
|
||||
}
|
||||
|
||||
public ResponseCookie getCleanJwtRefreshCookie() {
|
||||
return ResponseCookie.from(jwtRefreshCookie, null).path("/api/auth/refresh-token").build();
|
||||
}
|
||||
|
||||
public ResponseCookie generateJwtCookie(UserDetailsImpl userDetails) {
|
||||
String jwt = generateTokenFromUsername(userDetails.getUsername());
|
||||
return generateCookie(jwtCookie, jwt, "/api");
|
||||
}
|
||||
|
||||
public ResponseCookie generateJwtCookie(User user) {
|
||||
String jwt = generateTokenFromUsername(user.getUsername());
|
||||
return generateCookie(jwtCookie, jwt, "/api");
|
||||
}
|
||||
|
||||
public ResponseCookie generateRefreshJwtCookie(String refreshToken) {
|
||||
return generateCookie(jwtRefreshCookie, refreshToken, "/api/auth/refresh-token");
|
||||
}
|
||||
|
||||
|
||||
public String getUserNameFromJwtToken(String token) {
|
||||
return Jwts.parserBuilder().setSigningKey(key()).build()
|
||||
.parseClaimsJws(token).getBody().getSubject();
|
||||
}
|
||||
|
||||
private Key key() {
|
||||
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
|
||||
}
|
||||
|
||||
public String generateTokenFromUsername(String username) {
|
||||
return Jwts.builder()
|
||||
.setSubject(username)
|
||||
.setIssuedAt(new Date())
|
||||
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
|
||||
.signWith(SignatureAlgorithm.HS256, jwtSecret)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String getUserNameFromJwtToken(String token) {
|
||||
return Jwts.parserBuilder().setSigningKey(key()).build()
|
||||
.parseClaimsJws(token).getBody().getSubject();
|
||||
}
|
||||
|
||||
public boolean validateJwtToken(String authToken) {
|
||||
try {
|
||||
Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
|
||||
@@ -91,25 +71,4 @@ public class JwtUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String generateTokenFromUsername(String username) {
|
||||
return Jwts.builder()
|
||||
.setSubject(username)
|
||||
.setIssuedAt(new Date())
|
||||
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
|
||||
.signWith(key(), SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
private ResponseCookie generateCookie(String name, String value, String path) {
|
||||
return ResponseCookie.from(name, value).path(path).maxAge(24 * 60 * 60).httpOnly(true).build();
|
||||
}
|
||||
|
||||
private String getCookieValueByName(HttpServletRequest request, String name) {
|
||||
Cookie cookie = WebUtils.getCookie(request, name);
|
||||
if (cookie != null) {
|
||||
return cookie.getValue();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ spring.datasource.password=
|
||||
spring.jpa.hibernate.ddl-auto= update
|
||||
|
||||
# App Properties
|
||||
spring.app.jwtCookieName= cookie-jwt-for-security
|
||||
spring.app.jwtRefreshCookieName= cookie-refresh-jwt-for-security
|
||||
spring.app.jwtSecret= 9sL3p2mGzN7oR4Dx8QcY1uKwF5BhVtX6EaJgU3iZqOyMlIbCnAeHrWfPd0
|
||||
spring.app.jwtExpirationMs= 86400000
|
||||
spring.app.jwtSecret= ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
spring.app.jwtExpirationMs= 60000
|
||||
spring.app.jwtRefreshExpirationMs= 259200000
|
||||
Reference in New Issue
Block a user