Login Module
After completing all the previous modules, we continue with login validation, global exception handling, transaction management, and AOP.
A simple login checks the database for the employee. This can be implemented using a three-layer architecture.
Controller Layer
Create a LoginController class dedicated to handling login requests.
import com.itheima.pojo.Emp;
import com.itheima.pojo.Result;
import com.itheima.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("Employee login: {}", emp);
Emp e = empService.login(emp);
return e != null ? Result.success() : Result.error("Invalid username or password");
}
}
Service Layer
EmpService interface
public interface EmpService{
/**
* Handle employee login
*/
Emp login(Emp emp);
}
EmpServiceImpl
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
Note: The method name
getByUsernameAndPasswordis used at the Mapper layer, which directly interacts with the database, as opposed tologinwhich is a business layer method.
Mapper Layer
EmpMapper
@Mapper
public interface EmpMapper{
/**
* Find employee by username and password
*/
@Select("SELECT * FROM emp WHERE username = #{username} AND password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
}
This is a simple SQL statement, hence it is written directly as an annotation.
Now run the application. First, log out, then input the username and password (ensure they exist in the database). After clicking login, if it returns to the main page, the login function is complete.
Login Validation with JWT
The above login lacks validation. We need to implement login validation.
Why use JWT tokens? Compared to cookies and sessions, JWT provides greater advantages with fewer drawbacks.
- First, add the required dependencies in
pom.xml:
<!-- Servlet dependency -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<!-- JWT token -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
- Create a utility class
JwtUtilsin theutilspackage:
package com.itheima.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
// Signing key for signature algorithm
private static String signKey = "itheima";
// Token validity period in milliseconds
private static Long expire = 86400000L;
/**
* Generate JWT token
* @param claims Data to store in the JWT payload
* @return JWT token string
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* Parse JWT token
* @param jwt JWT token string
* @return Claims stored in the token payload
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
- Modify the
LoginController:
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("Employee login: {}", emp);
Emp e = empService.login(emp);
// On successful login, generate and return token
if (e != null) {
Map<String, Object> claims = new HashMap<>();
claims.put("id", e.getId());
claims.put("name", e.getName());
claims.put("username", e.getUsername());
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
// Return error if login fails
return Result.error("Invalid username or password");
}
}
Now, start the application and test the login endpoint with a POST request containing JSON with username and password. The response data should contain a token. When testing the department endpoint without the token in the header, it should fail. Add the token as a header parameter to verify correct functionality.
Interceptor
The interceptor intercepts requests before they reach the controller to validate login. Override the preHandle method.
Create a new package interceptor and within it, a LoginCheckInterceptor class.
package com.itheima.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.itheima.pojo.Result;
import com.itheima.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
log.info("Login validation...");
// 1. Get the request URL
String url = req.getRequestURL().toString();
log.info("Request URL: {}", url);
// 2. Skip login check if URL contains "login"
if (url.contains("login")) {
log.info("Login request, allow without check");
return true;
}
// 3. Fetch the token from header
String jwt = req.getHeader("token");
// 4. Check if token exists
if (!StringUtils.hasLength(jwt)) {
log.info("Token is missing, returning error");
Result error = Result.error("NOT_LOGIN");
String json = JSONObject.toJSONString(error);
resp.getWriter().write(json);
return false;
}
// 5. Try to parse token
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
log.error("Token parsing failed", e);
Result error = Result.error("NOT_LOGIN");
String json = JSONObject.toJSONString(error);
resp.getWriter().write(json);
return false;
}
// 6. Allow the request
log.info("Token is valid, allowing request");
return true;
}
}
Create a config package with a WebConfig class to configure interceptor paths:
package com.itheima.config;
import com.itheima.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
After logging out and logging in again, if the login is successful and data displays correctly, the interceptor is configured correctly.
Global Exception Handling
In controllers, adding try-catch blocks everywhere becomes cumbersome. A global exception handler can be used for unified error handling.
Create a package exception and a class GlobalExceptionHandler:
package com.itheima.exception;
import com.itheima.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* Handle all exceptions
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception ex){
ex.printStackTrace();
return Result.error("Operation failed. Please contact the system administrator.");
}
}
@ExceptionHandler specifies the exception handling method. When combined with @RestControllerAdvice, it globally handles controller exceptions. Exception.class captures all exceptions.
To test, intentionally cause an exception in the department controller (e.g., division by zero). A toast should appear indicating the global exception handler is working.
Transaction Management
Understanding transactions: When deleting a department, the employees belonging to that department should also be deleted. Both operations should either succeed or fail together.
In the service layer (DeptServiceImpl), add the @Transactional annotation to the delete method with rollbackFor = Exception.class to roll back on any exception.
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Integer id) {
// Delete department
deptMapper.deleteById(id);
// Delete employees in that department
empMapper.deleteByDeptId(id);
}
Enject the EmpMapper into the service class and add a method to delete employees by departmant ID:
@Mapper
public interface EmpMapper{
/**
* Delete employees by department id
*/
@Delete("DELETE FROM emp WHERE dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);
}
Remove the intentionally created exception. Run the program; deleting a department should also delete its employees.
AOP
AOP (Aspect-Oriented Programming) can be used for cross-cutting concerns like logging and transaction management.
We will use AOP to implement an operation log.
- Add AOP dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- Create an annotation
@MyLogin theannopackage:
package com.itheima.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}
- Create the entity class
OperateLog:
package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id;
private Integer operateUser;
private LocalDateTime operateTime;
private String className;
private String methodName;
private String methodParams;
private String returnValue;
private Long costTime;
}
- Create the database table:
CREATE TABLE operate_log (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 'ID',
operate_user INT UNSIGNED NULL COMMENT 'Operator ID',
operate_time DATETIME NULL COMMENT 'Operation time',
class_name VARCHAR(100) NULL COMMENT 'Class name',
method_name VARCHAR(100) NULL COMMENT 'Method name',
method_params VARCHAR(1000) NULL COMMENT 'Method parameters',
return_value VARCHAR(2000) NULL COMMENT 'Return value',
cost_time BIGINT NULL COMMENT 'Execution time in ms'
) COMMENT 'Operation log table';
- Create
OperateLogMapper:
package com.itheima.mapper;
import com.itheima.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OperateLogMapper {
@Insert("INSERT INTO operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"VALUES (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime})")
void insert(OperateLog log);
}
- Implement the AOP aspect in the
aoppackage:
package com.itheima.aop;
import com.alibaba.fastjson.JSONObject;
import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import com.itheima.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;
@Slf4j
@Component
@Aspect
public class LogAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.itheima.anno.MyLog)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
// Operator ID from JWT
String jwt = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(jwt);
Integer operatorUserId = (Integer) claims.get("id");
// Operation time
LocalDateTime operateTime = LocalDateTime.now();
// Class name
String className = joinPoint.getTarget().getClass().getName();
// Method name
String methodName = joinPoint.getSignature().getName();
// Method parameters
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);
// Execute the original method and get return value
long begin = System.currentTimeMillis();
Object result = joinPoint.proceed();
String returnValue = JSONObject.toJSONString(result);
long end = System.currentTimeMillis();
// Execution time
Long costTime = end - begin;
// Create log entry
OperateLog operateLog = new OperateLog(null, operatorUserId, operateTime, className, methodName, methodParams, returnValue, costTime);
operateLogMapper.insert(operateLog);
log.info("Operation log inserted: {}", operateLog);
return result;
}
}
- Apply the
@MyLogannotation to methods in the service implementations, such asDeptServiceImpl:
@Override
@MyLog
public List<Dept> list() {
return deptMapper.list();
}
@Override
@MyLog
public void save(Dept dept) {
deptMapper.save(dept);
}
After starting the application and performing operations, you should see log entries in the console and the database.