【Java】在 Spring Boot 中集成 Spring Security + JWT 实现基于 Token 的身份认证
目录
- 0 Spring Security简介
- 1 添加Maven依赖
- 2 创建相关实体类
- 2.1 User类实体
- 2.2 Role类实体
- 2.3 Permission类实体
- 3 创建数据访问层
- 4 JWT 实现
- 4.1 JwtAuthenticationEntryPoint 类
- 4.2 添加JWT属性
- 4.3 JwtTokenProvider 类
- 4.4 JwtAuthenticationFilter 类
- 4.5 CustomUserDetailsService 类
- 4.6 Spring Security 配置
- 5 创建服务层
- 5.1 验证服务层
- 5.2 用户服务层
- 6 创建控制层
- 7 启动项目
- 8 使用Postman测试接口
0 Spring Security简介
Spring Security是Spring家族中的一个安全管理认证与授权框架,是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于 Spring 的应用程序。侧重于为 Java 应用程序提供身份验证和授权。
与所有 Spring 项目一样,Spring 安全性的真正强大之处,在于它很容易扩展以满足定制需求。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
1 添加Maven依赖
本文基于SSM框架(可参考博客:【Java】使用IntelliJ IDEA搭建SSM(MyBatis-Plus)框架并连接MySQL数据库)
在pom.xml文件中添加依赖(包含在标签dependencies
中):
<!-- Spring Security -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency><!-- JWT -->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope>
</dependency><!-- 持久化操作 -->
<dependency><groupId>jakarta.persistence</groupId><artifactId>jakarta.persistence-api</artifactId><version>2.2.3</version>
</dependency>
2 创建相关实体类
创建实体类(entity),这里以创建User
类和Role
类为例,其中User与Role是多对一(Many-to-One)的关系,即多个用户(User)可以关联到同一个角色(Role),但每个用户只能属于一个角色。
2.1 User类实体
package com.z.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import java.io.Serializable;@Data
@TableName("user")
public class User implements Serializable {private static final long serialVersionUID = 1L;/**id*/@TableId(type = IdType.AUTO)@ApiModelProperty(value = "id")private Integer userId;@TableField("user_role_id")@ApiModelProperty(value = "用户角色") /* 1=用户 2=管理员 */private Integer userRoleId;@ApiModelProperty(value = "用户名")private String username;@ApiModelProperty(value = "密码")private String password;@ManyToOne@JoinColumn(name = "user_role_id")@TableField(exist = false)private Role roles;
}
2.2 Role类实体
package com.z.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import javax.persistence.Column;
import java.io.Serializable;@Data
@TableName("role")
public class Role implements Serializable {private static final long serialVersionUID = 1L;/**id*/@TableId(type = IdType.AUTO)@ApiModelProperty(value = "角色id")private Integer roleId;@ApiModelProperty(value = "角色名称")@Column(name = "role_name")private String roleName;@TableField(exist = false)@ApiModelProperty(value = "角色权限列表")private List<Permission> permissions;
}
2.3 Permission类实体
package com.z.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import java.io.Serializable;@Data
@TableName("permission")
public class Permission implements Serializable {private static final long serialVersionUID = 1L;/**id*/@TableId(type = IdType.AUTO)@ApiModelProperty(value = "权限id")private Integer permissionId;@ApiModelProperty(value = "权限资源")private String permissionResources;@ApiModelProperty(value = "权限名称")private String permissionName;
}
3 创建数据访问层
创建数据访问层(mapper),UserMapper
和 RoleMapper
。
UserMapper.java:
package com.z.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.z.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface UserMapper extends BaseMapper<User> {User findByUsername(String username);User getRoleByUserId(Integer userId);List<User> findUsersByName(@Param("name") String name);
}
RoleMapper.java:
package com.z.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.z.entity.Role;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface RoleMapper extends BaseMapper<Role> {Role findByUserId(Integer userId);List<Permission> findPermissionsByRoleId(@Param("roleId") Integer roleId);
}
创建对应的XML文件:
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="com.z.mapper.UserMapper"><select id="findByUsername" resultType="com.z.entity.User">SELECT *FROM userWHERE username = #{username}</select><select id="getRoleByUserId" resultType="com.z.entity.User">SELECT *FROM userWHERE user_id = #{userId}</select><select id="findUsersByName" resultType="com.z.entity.User">SELECT *FROM userWHERE name LIKE CONCAT('%', #{name}, '%')</select></mapper>
RoleMapper.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="com.z.mapper.RoleMapper"><!-- 根据用户ID查询角色信息 --><select id="findByUserId" resultType="com.z.entity.Role">SELECT r.*FROM role rINNER JOIN user u ON r.role_id = u.user_role_idWHERE u.user_id = #{userId}</select><!-- 根据角色ID查询角色权限 --><select id="findPermissionsByRoleId" resultType="com.z.entity.Permission">SELECT p.*FROM permission pINNER JOIN role_permission rp ON p.permission_id = rp.permission_idWHERE rp.role_id = #{roleId}</select>
</mapper>
4 JWT 实现
在 Spring Boot 项目中创建一个 security
包,并添加以下与 JWT 相关的类和属性。
4.1 JwtAuthenticationEntryPoint 类
JwtAuthenticationEntryPoint
类实现了AuthenticationEntryPoint
接口。
AuthenticationEntryPoint
由 ExceptionTranslationFilter
来启动身份认证方案。它是一个入口点,用于检查用户是否已通过身份认证,如果用户已经认证,则登录该用户,否则抛出异常(unauthorized)。
通常情况下,在简单的应用程序中可以直接使用该接口的默认实现类(如 LoginUrlAuthenticationEntryPoint
),但当在 REST、JWT 等中使用 Spring Security 时,就必须实现AuthenticationEntryPoint
接口,重写 commence()
方法,在此方法中定义如何返回 JSON 格式的 401 错误以提供更好的 Spring Security 过滤器链(filter chain)管理。
package com.z.security;import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());}
}
4.2 添加JWT属性
在 application.yml
中添加两个 JWT 相关属性,分别用于定义 JWT 签名密钥 和 Token 有效期:
app:jwt-secret: daf66e01593f61a15b857cf433aae03a005812b31234e149036bcc8dee755dbbjwt-expiration-milliseconds: 604800000 #七天
4.3 JwtTokenProvider 类
创建一个 JwtTokenProvider
工具类,用于生成、验证 JWT 以及从 JWT 中提取信息。
package com.z.security;import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;import java.security.Key;
import java.util.Date;@Component
public class JwtTokenProvider {private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);@Value("${app.jwt-secret}")private String jwtSecret;@Value("${app.jwt-expiration-milliseconds}")private long jwtExpirationDate;// 生成 JWT tokenpublic String generateToken(Authentication authentication){String username = authentication.getName();Date currentDate = new Date();Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);String token = Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(expireDate).signWith(key()).compact();return token;}private Key key(){return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));}// 从 Jwt token 获取用户名public String getUsername(String token){Claims claims = Jwts.parserBuilder().setSigningKey(key()).build().parseClaimsJws(token).getBody();String username = claims.getSubject();return username;}// 验证 Jwt tokenpublic boolean validateToken(String token){try {Jwts.parser().setSigningKey(key()).parseClaimsJws(token).getBody();return true;} catch (ExpiredJwtException e) {logger.error("The JWT token is expired: " + e.getMessage());return false; // 标记为过期的 JWT} catch (UnsupportedJwtException e) {logger.error("Unsupported JWT type: " + e.getMessage());return false; // 不支持的 JWT 类型} catch (MalformedJwtException e) {logger.error("Malformed JWT token: " + e.getMessage());return false; // 格式错误的 JWT} catch (SignatureException e) {logger.error("Signature validation failed: " + e.getMessage());return false; // 签名验证失败的 JWT} catch (IllegalArgumentException e) {logger.error("Invalid JWT: " + e.getMessage());return false; // 其他非法情况}}
}
其中:
-
generateToken(Authentication authentication)
方法根据提供的 Authentication 对象生成一个新的 JWT,该对象包含被验证用户的信息。它使用 Jwts.builder() 方法创建一个新的 JwtBuilder 对象,设置 JWT 的 subject(即用户名)、发布日期(issue date)和到期日期(expiration date),并使用key()
方法对 JWT 进行签名。最后,它会以字符串形式返回 JWT。 -
getUsername(String token)
方法从提供的 JWT 中提取 username。该方法使用Jwts.parserBuilder()
方法创建一个新的 JwtParserBuilder 对象,使用key()
方法设置签名密钥(Signing Key),并使用parseClaimsJws()
方法解析 JWT。然后,它会从 JWT 的 Claims 对象中获取 subject(即用户名),并以字符串形式返回。 -
validateToken(String token)
方法会验证所提供的 JWT。该方法使用Jwts.parserBuilder()
方法创建一个新的 JwtParserBuilder 对象,使用key()
方法设置签名密钥,并使用parse()
方法解析 JWT。如果 JWT 有效,该方法会返回 true。如果 JWT 无效或已过期,该方法会使用 logger 对象输出错误信息并返回 false。
4.4 JwtAuthenticationFilter 类
创建一个 JwtAuthenticationFilter
类,该类可拦截传入的 HTTP 请求并验证包含在 Authorization 头中的 JWT Token。如果 Token 有效,Filter 就会在 SecurityContext 中设置当前用户的 Authentication。
package com.z.security;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {private JwtTokenProvider jwtTokenProvider;private UserDetailsService userDetailsService;public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) {this.jwtTokenProvider = jwtTokenProvider;this.userDetailsService = userDetailsService;}@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {// 从 request 获取 JWT tokenString token = getTokenFromRequest(request);// 校验 tokenif(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)){// 从 token 获取 usernameString username = jwtTokenProvider.getUsername(token);// 加载与 token 关联的用户UserDetails userDetails = userDetailsService.loadUserByUsername(username);UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);}// 验证通过,继续处理请求filterChain.doFilter(request, response);}private String getTokenFromRequest(HttpServletRequest request){String bearerToken = request.getHeader("Authorization");if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")){return bearerToken.substring(7, bearerToken.length());}return null;}
}
- 该类继承了 Spring 的
OncePerRequestFilter
,可确保每个请求只执行一次过滤器。 - 构造函数需要两个依赖:
JwtTokenProvider
和UserDetailsService
,它们是通过 Spring 的构造函数依赖注入机制注入的。 doFilterInternal
方法是 Filter 的主要逻辑。它使用getTokenFromRequest
方法从 Authorization Header 中提取 JWT Token,使用JwtTokenProvider
类验证 Token,并在 SecurityContextHolder 中设置 Authentication 信息。getTokenFromRequest
方法会解析 Authorization Header,并返回 Token 部分。- SecurityContextHolder 用于存储当前 request 的 Authentication 信息。在这种情况下,Filter 会将 UsernamePasswordAuthenticationToken 与该 Token 关联的 UserDetails 和 authorities(授权)设置在一起。
4.5 CustomUserDetailsService 类
创建一个 CustomUserDetailsService
类,它实现了 UserDetailsService
接口(Spring Security 内置接口),并提供了 loadUserByUername()
方法的实现:
package com.z.service.impl;import com.z.entity.Permission;
import com.z.entity.Role;
import com.z.entity.User;
import com.z.mapper.RoleMapper;
import com.z.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleMapper roleMapper;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {User user = userMapper.findByUsername(s);if (user == null) {throw new UsernameNotFoundException("没有该用户");}Role role = roleMapper.findByUserId(user.getUserId());if (role == null) {throw new UsernameNotFoundException("该用户没有权限");}List<Permission> permissions = roleMapper.findPermissionsByRoleId(role.getRoleId());return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),getAuthorities(permissions));}private Collection<? extends GrantedAuthority> getAuthorities(List<Permission> permissions) {return permissions.stream().map(permission -> new SimpleGrantedAuthority(permission.getPermissionName())).collect(Collectors.toList());}}
4.6 Spring Security 配置
创建 SpringSecurityConfig
类,并添加以下配置:
package com.z.security;import com.z.service.impl.CustomUserDetailsService;
import io.jsonwebtoken.*;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate CustomUserDetailsService userDetailsService;@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Overrideprotected void configure(HttpSecurity http) throws Exception {//http.cors().and().csrf().disable()http.csrf().disable().authorizeRequests().antMatchers("/api/auth/**").permitAll().antMatchers("/user/**").authenticated() // 需要token验证的API路径.antMatchers("/role/**").authenticated().antMatchers("/permission/**").authenticated().anyRequest().authenticated().and()//.addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class).exceptionHandling().accessDeniedHandler(getAccessDeniedHandler()).authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))/*.and().formLogin()*/.and().logout().invalidateHttpSession(true) // 无效化HTTP会话.deleteCookies("JSESSIONID") // 删除指定的cookie(可选).permitAll(); // 允许所有用户访问}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/resources/**"); // 忽略静态资源web.ignoring().antMatchers("/index.html","/login_page","favicon.icon","/static/**");}// 自定义Token验证过滤器private static class TokenAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate CustomUserDetailsService userDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = extractTokenFromRequest(request);if (token != null && performAuthentication(token)) {// 如果token有效,将token转换为认证信息,并将其设置到SecurityContext中Authentication auth = new UsernamePasswordAuthenticationToken(token, null, Collections.emptyList());SecurityContextHolder.getContext().setAuthentication(auth);System.err.println(SecurityContextHolder.getContext().getAuthentication());}filterChain.doFilter(request, response);}private Boolean performAuthentication(String token) {try {String username = validateAndParseToken(token);UserDetails userDetails = userDetailsService.loadUserByUsername(username);UsernamePasswordAuthenticationToken authentication = newUsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities());System.err.println(authentication);return true;} catch (Exception e) {// 处理验证失败的情况return false;}}private String validateAndParseToken(String token) {try {// 解析token并验证签名Claims claims = Jwts.parserBuilder().setSigningKey(Keys.secretKeyFor(SignatureAlgorithm.HS256)).build().parseClaimsJws(token).getBody();// 从claims中获取用户名信息,此处假设用户名存储在subject中return claims.getSubject();} catch (Exception e) {// 捕获验证失败的异常,并在需要时进行处理或记录throw new RuntimeException("Invalid token");}}private String extractTokenFromRequest(HttpServletRequest request) {String authorizationHeader = request.getHeader("Authorization");if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {return authorizationHeader.substring(7);}return null;}}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {return configuration.getAuthenticationManager();}/*** 权限不足* @return*/@BeanAccessDeniedHandler getAccessDeniedHandler(){return new AuthenticationAccessDeniedHandler();}
}
-
@Configuration
注解表示该类定义了 Spring Application Context 的配置。 -
@AllArgsConstructor
注解来自 Lombok 库,它会生成一个包含所有用@NonNull
注解的字段的构造函数。 -
passwordEncoder()
方法是一个 Bean,用于创建 BCryptPasswordEncoder 实例,对密码进行编码。 -
securityFilterChain()
方法是一个定义安全过滤器链(Security Filter Chain)的 Bean。HttpSecurity 参数用于配置应用程序的安全设置。在本例中,该方法禁用 CSRF 保护,并根据 HTTP 方法和 URL 授权请求。 -
authenticationManager()
方法是一个提供 AuthenticationManager 的 Bean。它从 AuthenticationConfiguration 实例中检索 Authentication Manager。
其中,AuthenticationAccessDeniedHandler
类如下:
package com.z.security;import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {resp.setStatus(HttpServletResponse.SC_FORBIDDEN);resp.setContentType("text/html;charset=utf-8");PrintWriter out = resp.getWriter();out.write("权限不足,请联系管理员!");out.flush();out.close();}
}
5 创建服务层
5.1 验证服务层
创建验证服务层(service)及其实现,AuthService
接口 和 AuthServiceImpl
类。
AuthService 接口:
package com.z.service;import com.z.dto.UserLoginRequestDTO;public interface AuthService {String login(UserLoginRequestDTO loginDto);
}
其中 UserLoginRequestDTO
如下,用于用户登录输入信息:
package com.z.dto;import lombok.Data;@Data
public class UserLoginRequestDTO {private String username;private String password;
}
AuthServiceImpl类:
package com.z.service.impl;import com.z.dto.UserLoginRequestDTO;
import com.z.mapper.UserMapper;
import com.z.security.JwtTokenProvider;
import com.z.service.AuthService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;@Service
public class AuthServiceImpl implements AuthService {private AuthenticationManager authenticationManager;private UserMapper userMapper;private PasswordEncoder passwordEncoder;private JwtTokenProvider jwtTokenProvider;public AuthServiceImpl(JwtTokenProvider jwtTokenProvider,UserMapper userMapper,PasswordEncoder passwordEncoder,AuthenticationManager authenticationManager) {this.authenticationManager = authenticationManager;this.userMapper = userMapper;this.passwordEncoder = passwordEncoder;this.jwtTokenProvider = jwtTokenProvider;}@Overridepublic String login(UserLoginRequestDTO loginDto) {Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()));SecurityContextHolder.getContext().setAuthentication(authentication);String token = jwtTokenProvider.generateToken(authentication);return token;}}
AuthService
接口的实现 AuthServiceImpl
,包含一个方法 login()
,用于处理应用程序的登录功能。loginDto 对象包含用户输入的用户名(username)和密码(password)。
该类的构造函数需要四个参数:JwtTokenProvider、UserRepository、PasswordEncoder 和 AuthenticationManager。
在 login()
方法中,authenticationManager 会尝试将用户的 loginDto 凭证传递给 UsernamePasswordAuthenticationToken,从而对用户进行身份认证。如果认证成功,将使用 jwtTokenProvider 对象生成一个 Token 并返回给调用者。
5.2 用户服务层
创建用户服务层(service)及其实现,UserService
接口 和 UserServiceImpl
类。
UserService 接口:
package com.z.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.z.entity.User;public interface UserService extends IService<User> {// 用户注册void registerUser(User user);// 查找是否用户名已经存在User findByUsername(String username);
}
UserServiceImpl 类:
package com.z.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.z.entity.User;
import com.z.mapper.UserMapper;
import com.z.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.security.crypto.password.PasswordEncoder;@Primary
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate PasswordEncoder passwordEncoder; // 注入BCryptPasswordEncoder/*** 用户注册* @param user*/public void registerUser(User user) {// 使用BCrypt加密密码String encryptedPassword = passwordEncoder.encode(user.getPassword());user.setPassword(encryptedPassword);userMapper.insert(user);}@Overridepublic User findByUsername(String username) {return userMapper.findByUsername(username);}
}
6 创建控制层
创建AuthController
层,处理用户注册、登录、登出等业务逻辑:
package com.z.controller;import com.z.dto.UserRegisterRequestDTO;
import com.z.entity.User;
import com.z.security.JwtTokenProvider;
import com.z.service.UserService;
import com.z.utils.ApiResult;
import lombok.AllArgsConstructor;
import com.z.dto.JWTAuthResponse;
import com.z.dto.UserLoginRequestDTO;
import com.z.service.AuthService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@AllArgsConstructor
@RestController
@RequestMapping("/api/auth")
public class AuthController {private AuthService authService;private UserService userService;private JwtTokenProvider jwtTokenProvider;// Login REST API@PostMapping("/login")public ApiResult authenticate(@RequestBody UserLoginRequestDTO loginDto){// 检查用户名是否为空if (loginDto.getUsername() == null || loginDto.getUsername().isEmpty()) {return ApiResult.error("用户名不能为空");}// 检查密码是否为空if (loginDto.getPassword() == null || loginDto.getPassword().isEmpty()) {return ApiResult.error("密码不能为空");}String token = authService.login(loginDto);JWTAuthResponse jwtAuthResponse = new JWTAuthResponse();String username = jwtTokenProvider.getUsername(token);User user = userService.findByUsername(username);if (user != null) {jwtAuthResponse.setAccessToken(token);jwtAuthResponse.setUserInfo(user);return ApiResult.ok("登录成功",jwtAuthResponse);}else{return ApiResult.unauthorized("用户未注册/用户名或密码错误");}}@PostMapping("/logout")public ApiResult logout(HttpServletRequest request, HttpServletResponse response) {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null) {new SecurityContextLogoutHandler().logout(request, response, authentication);}return ApiResult.ok("登出成功",authentication);}@PostMapping("/register")public ApiResult registerUser(@RequestBody UserRegisterRequestDTO userRegisterRequestDTO) {User user = new User();user.setUsername(userRegisterRequestDTO.getUsername());user.setPassword(userRegisterRequestDTO.getPassword());user.setName(userRegisterRequestDTO.getName());// 检查用户名是否为空if (userRegisterRequestDTO.getUsername() == null || userRegisterRequestDTO.getUsername().isEmpty()) {return ApiResult.error("用户名不能为空");}// 检查用户名是否已存在else if (userService.findByUsername(userRegisterRequestDTO.getUsername()) != null) {return ApiResult.error("用户名已存在");}else {userService.registerUser(user);return ApiResult.ok("注册成功",user);}}
}
其中,ApiResult
、UserRegisterRequestDTO
、JWTAuthResponse
分别如下:
ApiResult.java:
package com.z.utils;
import lombok.Data;import java.util.List;@Datapublic class ApiResult {// 定义状态码public static final int OK = 200;public static final int ERROR = 500;public static final int Unauthorized = 401;public static final int Invalid = 404;// 定义返回结果的字段private int code;private String message;private Object data;// 构造器public ApiResult(int code, String message, Object data) {this.code = code;this.message = message;this.data = data;}// 静态方法创建成功的响应public static ApiResult ok(String message, Object data) {return new ApiResult(OK, message, data);}// 静态方法创建错误的响应public static ApiResult error(String message) {return new ApiResult(ERROR, message, null);}//未授权public static ApiResult unauthorized(String message) { return new ApiResult(Unauthorized, message,null); }public static ApiResult violateConstraint(List<String> violation) {return new ApiResult(Invalid, "参数校验未通过", violation);}
}
UserRegisterRequestDTO.java:
package com.z.dto;import lombok.Data;@Data
public class UserRegisterRequestDTO {private String username;private String password;private String name;
}
JWTAuthResponse.java:
package com.z.dto;import com.z.entity.User;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class JWTAuthResponse {private String accessToken;private String tokenType = "Bearer";private User userInfo;
}
7 启动项目
编写Main.java运行项目,并通过IDEA的启动按钮启动项目:
package com.z;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class Main {public static void main(String[] args) {SpringApplication.run(Main.class, args);}
}
8 使用Postman测试接口
在MySQL数据库中新建一个数据库,并新增四张数据表user
、role
和permission
、role_permission
。
注册功能register
接口测试:
登录功能login
接口测试:
登出功能logout
接口测试: