In modern application development, secure authentication and authorization are crucial components. This article demonstrates how to implement a Login Service using:
- Spring Boot (REST API, MVC pattern)
- Spring Security + Authorization Server (OAuth2.1)
- MyBatis (database persistence)
- MySQL (user and token storage)
The system acts as:
- OAuth2 Authorization Server (issue JWT access tokens & refresh tokens).
- OAuth2 Client (support login with Google in the future).
- Resource Server validator (other services can validate tokens issued here).
Security Implementation Overview
- Authentication Methods:
- Primary: Username + password from MySQL database
- Secondary: OAuth2 Client (Google login - optional implementation)
- Authorization Framework:
- Token format: JWT.
- Access Token: short-lived (15 min).
- On successful login: Issue Access Token + Refresh Token.
- Refresh Token: stored in DB, valid for 7 days.
- On refresh token request: Validate token from DB.
- Store Refresh Token in
oauth_refresh_tokens
. - If valid, issue new Access Token and optionally rotate Refresh Token.
- On logout: Invalidate refresh token from DB.
- Supported grant types:
authorization_code
refresh_token
client_credentials
(for service-to-service auth)
Note: Since we’re using Spring Authorization Server (not yet part of the official Spring Boot suite but well-supported in Spring Boot 2.7+), we need to ensure version compatibility. We’ll use Spring Boot 3.x, which is compatible with Spring Authorization Server according to Spring’s official documentation.
Development Environment Setup
VS Code Extensions
Spring Boot Extension Pack (includes Spring Boot Tools, Spring Initializr, Spring Boot Dashboard)
Lombok Annotations Support
Java Extension Pack (includes Maven, Debugger, Test Runner)
REST Client (for API testing)
JDK Recommendation
Use JDK 21 (LTS) as Spring Boot 3.x defaults to Java 17+ support.
Project Implementation
Maven Dependencies
<?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>login-service</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<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>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Database Schema
User and auth refresh token table schema
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'USER',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
refresh_token VARCHAR(500) NOT NULL,
username VARCHAR(50) NOT NULL,
expiry_date TIMESTAMP NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE,
INDEX idx_refresh_token (refresh_token(100)),
INDEX idx_username (username)
);
Entity Classes
User entity
// entity/User.java
package auth.hoohoo.top.auth_service.entity;
import java.time.LocalDateTime;
import lombok.Data;
@Data
public class User {
private Long id;
private String username;
private String password;
private String role;
private Boolean enabled;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
Refresh token entity
// entity/RefreshToken.java
package auth.hoohoo.top.auth_service.entity;
import java.time.LocalDateTime;
import lombok.Data;
@Data
public class RefreshToken {
private Long id;
private Long userId;
private String token;
private LocalDateTime expiryDate;
private String username;
private Boolean revoked;
}
MyBatis Mapper Interfaces
User Mapper
// mapper/UserMapper.java
package auth.hoohoo.top.auth_service.mapper;
import java.util.Optional;
import org.apache.ibatis.annotations.Mapper;
import auth.hoohoo.top.auth_service.entity.User;
@Mapper
public interface UserMapper {
Optional<User> findByUsername(String username);
void save(User user);
void update(User user);
boolean existsByUsername(String username);
}
Refresh token mapper
// mapper/RefreshTokenMapper.java
package auth.hoohoo.top.auth_service.mapper;
import java.util.Optional;
import org.apache.ibatis.annotations.Mapper;
import auth.hoohoo.top.auth_service.entity.RefreshToken;
@Mapper
public interface RefreshTokenMapper {
Optional<RefreshToken> findByToken(String token);
Optional<RefreshToken> findByUsername(String username);
void save(RefreshToken refreshToken);
void update(RefreshToken refreshToken);
void revokeByUsername(String username);
void deleteExpiredTokens();
}
MyBatis Mapper XML Files
Mapper XML (resources/mapper/UserMapper.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="auth.hoohoo.top.auth_service.mapper.UserMapper">
<resultMap id="UserResultMap" type="auth.hoohoo.top.auth_service.entity.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="role" column="role"/>
<result property="enabled" column="enabled"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<select id="findByUsername" resultMap="UserResultMap">
SELECT * FROM users WHERE username = #{username}
</select>
<insert id="save" parameterType="auth.hoohoo.top.auth_service.entity.User"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (username, password, role, enabled)
VALUES (#{username}, #{password}, #{role}, #{enabled})
</insert>
<update id="update" parameterType="auth.hoohoo.top.auth_service.entity.User">
UPDATE users
SET password = #{password}, role = #{role}, enabled = #{enabled}
WHERE id = #{id}
</update>
<select id="existsByUsername" resultType="boolean">
SELECT COUNT(*) > 0 FROM users WHERE username = #{username}
</select>
</mapper>
Mapper XML (resources/mapper/RefreshTokenMapper.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="auth.hoohoo.top.auth_service.mapper.RefreshTokenMapper">
<resultMap id="RefreshTokenResultMap" type="auth.hoohoo.top.auth_service.entity.RefreshToken">
<id property="id" column="id"/>
<result property="token" column="token"/>
<result property="username" column="username"/>
<result property="expiryDate" column="expiry_date"/>
<result property="revoked" column="revoked"/>
<result property="userId" column="user_id"/>
</resultMap>
<select id="findByToken" resultMap="RefreshTokenResultMap">
SELECT * FROM oauth_refresh_tokens
WHERE token = #{token} AND revoked = FALSE
</select>
<select id="findByUsername" resultMap="RefreshTokenResultMap">
SELECT * FROM oauth_refresh_tokens
WHERE username = #{username} AND revoked = FALSE
</select>
<insert id="save" parameterType="auth.hoohoo.top.auth_service.entity.RefreshToken"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO oauth_refresh_tokens (token, username, expiry_date, revoked, user_id)
VALUES (#{token}, #{username}, #{expiryDate}, #{revoked}, #{userId})
</insert>
<update id="update" parameterType="auth.hoohoo.top.auth_service.entity.RefreshToken">
UPDATE oauth_refresh_tokens
SET revoked = #{revoked}
WHERE id = #{id}
</update>
<update id="revokeByUsername">
UPDATE oauth_refresh_tokens
SET revoked = TRUE
WHERE username = #{username} AND revoked = FALSE
</update>
<delete id="deleteExpiredTokens">
DELETE FROM oauth_refresh_tokens
WHERE expiry_date < NOW()
</delete>
</mapper>
DTO Classes
Login reqeust DTO
// dto/LoginRequest.java
package auth.hoohoo.top.auth_service.dto;
public record LoginRequest(String username, String password) {}
Token response DTO
// dto/TokenResponse.java
package auth.hoohoo.top.auth_service.dto;
public record TokenResponse(String accessToken, String refreshToken, String tokenType, long expiresIn) {}
Refresh token request DTO
// dto/RefreshTokenRequest.java
package auth.hoohoo.top.auth_service.dto;
public record RefreshTokenRequest(String refreshToken) {
}
User info DTO
// dto/UserInfo.java
package auth.hoohoo.top.auth_service.dto;
public record UserInfo(String username, String role) {}
API error DTO
// dto/ApiError.java
package auth.hoohoo.top.auth_service.dto;
import java.time.LocalDateTime;
public record ApiError(String error, String message, LocalDateTime timestamp) {
public ApiError(String error, String message) {
this(error, message, LocalDateTime.now());
}
}
Service Layer
User service
// service/UserService.java
package auth.hoohoo.top.auth_service.service;
import java.util.Collections;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import auth.hoohoo.top.auth_service.entity.User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import auth.hoohoo.top.auth_service.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userMapper.findByUsername(username)
.map(user -> new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
true, true, true,
Collections.singletonList(() -> "ROLE_" + user.getRole()))
).orElseThrow(() -> new UsernameNotFoundException("User not found:" + username));
}
@Transactional
public void createUser(String username, String password, String role) {
if (userMapper.existsByUsername(username)) {
throw new IllegalArgumentException("Username already exists");
}
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setRole(role);
user.setEnabled(true);
userMapper.save(user);
}
}
Login service
// service/TokenService.java
package auth.hoohoo.top.auth_service.service;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import auth.hoohoo.top.auth_service.entity.RefreshToken;
import auth.hoohoo.top.auth_service.mapper.RefreshTokenMapper;
import lombok.RequiredArgsConstructor;
import auth.hoohoo.top.auth_service.mapper.UserMapper;
@Service
@RequiredArgsConstructor
public class TokenService {
private final JwtEncoder JwtEncoder;
private final RefreshTokenMapper refreshTokenMapper;
private final UserMapper userMapper;
public String generateAccessToken(String username, String role) {
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(15, ChronoUnit.MINUTES))
.subject(username)
.claim("scope", role)
.build();
return JwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
@Transactional
public String generateRefreshToken(String username) {
//revoke current refresh token
refreshTokenMapper.revokeByUsername(username);
//create new refreesh token
RefreshToken refreshToken = new RefreshToken();
refreshToken.setToken(UUID.randomUUID().toString());
refreshToken.setUsername(username);
refreshToken.setExpiryDate(java.time.LocalDateTime.ofInstant(
Instant.now().plus(7, ChronoUnit.DAYS),
java.time.ZoneId.systemDefault()
));
refreshToken.setRevoked(false);
userMapper.findByUsername(username).ifPresent(user -> refreshToken.setUserId(user.getId()));
refreshTokenMapper.save(refreshToken);
return refreshToken.getToken();
}
@Transactional(readOnly = true)
public boolean validateRefreshToken(String token) {
return refreshTokenMapper.findByToken(token)
.map(refreshToken -> {
java.time.LocalDateTime now = java.time.LocalDateTime.ofInstant(
Instant.now(), java.time.ZoneId.systemDefault());
return !refreshToken.getRevoked() &&
refreshToken.getExpiryDate().isAfter(now);
})
.orElse(false);
}
@Transactional
public void revokeRefreshToken(String token) {
refreshTokenMapper.findByToken(token).ifPresent(refreshToken -> {
refreshToken.setRevoked(true);
refreshTokenMapper.update(refreshToken);
});
}
@Transactional
public void revokeAllUserTokens(String username) {
refreshTokenMapper.revokeByUsername(username);
}
public String getUsernameFromToken(String token) {
return refreshTokenMapper.findByToken(token)
.map(RefreshToken::getUsername)
.orElseThrow(()->new IllegalArgumentException("invlaid refresh token"));
}
}
Controller Layer
Auth controller
// controller/AuthController.java
package auth.hoohoo.top.auth_service.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// import com.nimbusds.oauth2.sdk.TokenResponse;
// Ensure this import matches the actual package and class name of TokenResponse
import auth.hoohoo.top.auth_service.dto.TokenResponse;
import auth.hoohoo.top.auth_service.dto.LoginRequest;
import auth.hoohoo.top.auth_service.dto.RefreshTokenRequest;
import auth.hoohoo.top.auth_service.service.TokenService;
import auth.hoohoo.top.auth_service.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {
private final AuthenticationManager authenticationManager;
private final TokenService tokenService;
private final UserService userService;
@PostMapping("/login")
public ResponseEntity<TokenResponse> login(@Valid @RequestBody LoginRequest request) {
log.info("Login request for user: {}", request.username());
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(),
request.password()));
String username = authentication.getName();
String role = authentication.getAuthorities().iterator().next().getAuthority().replace("ROLE_", "");
String accessToken = tokenService.generateAccessToken(username, role);
String refreshToken = tokenService.generateRefreshToken(username);
return ResponseEntity.ok(new TokenResponse(
accessToken,
refreshToken,
"Bearer",
15 * 60));
}
@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refreshToken(@Valid @RequestBody RefreshTokenRequest request) {
if (!tokenService.validateRefreshToken(request.refreshToken())) {
return ResponseEntity.badRequest().build();
}
String username = tokenService.getUsernameFromToken(request.refreshToken());
String role = "USER";
String accessToken = tokenService.generateAccessToken(username, role);
return ResponseEntity.ok(new TokenResponse(
accessToken,
request.refreshToken(),
"Bearer",
15 * 60));
}
@PostMapping("/logout")
public ResponseEntity<Void> logout(Authentication authentication) {
String username = authentication.getName();
tokenService.revokeAllUserTokens(username);
return ResponseEntity.ok().build();
}
@PostMapping("/register")
public ResponseEntity<Void> register(@Valid @RequestBody LoginRequest request) {
userService.createUser(request.username(), request.password(), "USER");
return ResponseEntity.ok().build();
}
}
User controller
// controller/UserController.java
package auth.hoohoo.top.auth_service.controller;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import auth.hoohoo.top.auth_service.dto.UserInfo;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
@GetMapping("/me")
public UserInfo getCurrentUser(Authentication authentication) {
String role = authentication.getAuthorities().iterator().next().getAuthority().replace("ROLE_", "");
return new UserInfo(authentication.getName(), role);
}
}
Security Configuration
Setting Spring security and OAuth2.0
// config/SecurityConfig.java
package auth.hoohoo.top.auth_service.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.http.SessionCreationPolicy;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.security.interfaces.RSAPrivateKey;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.proc.SecurityContext;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import auth.hoohoo.top.auth_service.service.UserService;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserService userService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/auth/login", "/auth/register", "/auth/refresh", "/health").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {}))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
)
.userDetailsService(userService);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public KeyPair keyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException("Unable to generate key pair", ex);
}
}
@Bean
public JwtEncoder jwtEncoder() {
JWKSet jwkSet = new JWKSet(createRsaKey());
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(jwkSet);
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair().getPublic()).build();
}
private RSAKey createRsaKey() {
KeyPair keyPair = keyPair();
return new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey((RSAPrivateKey) keyPair.getPrivate())
.keyID(UUID.randomUUID().toString())
.build();
}
}
Exception Handling
Global exception handler
// config/GlobalExceptionHandler.java
package auth.hoohoo.top.auth_service.config;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import auth.hoohoo.top.auth_service.dto.ApiError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiError> handleBadCredentialsException(BadCredentialsException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ApiError("invalid_credentials", "Username or password error"));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiError> handleIllegalArgumentException(IllegalArgumentException ex) {
return ResponseEntity.badRequest()
.body(new ApiError("bad_request", ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGenericException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ApiError("internal_error", "Server error"));
}
}
Main Application Class
// LoginServiceApplication.java
package auth.hoohoo.top.auth_service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AuthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServiceApplication.class, args);
}
}
Application Configuration
# application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/login_service
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema.sql
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.loginservice.entity
logging.level.com.example.loginservice=DEBUG
Testing Implementation
// src/test/java/AuthServiceApplicationTests.java
package auth.hoohoo.top.auth_service;
import org.springframework.beans.factory.annotation.Autowired;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import auth.hoohoo.top.auth_service.dto.LoginRequest;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.http.MediaType;
@SpringBootTest
@AutoConfigureMockMvc
class AuthServiceApplicationTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void contextLoads() {
}
@Test
void testRegisterAndLogin() throws Exception {
// 註冊新使用者
LoginRequest registerRequest = new LoginRequest("testuser", "password123");
mockMvc.perform(post("/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isOk());
// 測試登入
mockMvc.perform(post("/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isOk());
}
}
Running and Testing the Application
Ensure MySQL database is running with a login_service database created
Start the application using Maven: mvn spring-boot:run
Test the API endpoints:
✦ Here are the URLs and Postman interaction methods:
Register a New User
curl --location 'http://localhost:8080/ auth/register' \--header 'Content-Type: application/json' \--data ' { "username": "your_username", "password": "your_password" }'
Login
curl --location 'http://localhost:8080/auth/login' \
--header 'Content-Type: application/json' \
--data '{
"username": "your_username",
"password": "your_password"
}'
Refresh token
curl --location --request POST 'http://localhost:8080/auth/logout' \
--header 'Authorization: Bearer access-token'
This implementation provides a robust OAuth2 authorization server using Spring Boot and MyBatis, with JWT tokens for secure authentication and authorization. The architecture supports