目录
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 user
WHERE username = #{username}
</select>
<select id="getRoleByUserId" resultType="com.z.entity.User">
SELECT *
FROM user
WHERE user_id = #{userId}
</select>
<select id="findUsersByName" resultType="com.z.entity.User">
SELECT *
FROM user
WHERE 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 r
INNER JOIN user u ON r.role_id = u.user_role_id
WHERE u.user_id = #{userId}
</select>
<!-- 根据角色ID查询角色权限 -->
<select id="findPermissionsByRoleId" resultType="com.z.entity.Permission">
SELECT p.*
FROM permission p
INNER JOIN role_permission rp ON p.permission_id = rp.permission_id
WHERE 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 {
@Override
public 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: daf66e01593f61a15b857cf433aae03a005812b31234e149036bcc8dee755dbb
jwt-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 token
public 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 token
public 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;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 从 request 获取 JWT token
String token = getTokenFromRequest(request);
// 校验 token
if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)){
// 从 token 获取 username
String 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 {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public 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 {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected 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(); // 允许所有用户访问
}
@Override
public 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 {
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected 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 = new
UsernamePasswordAuthenticationToken(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;
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
/**
* 权限不足
* @return
*/
@Bean
AccessDeniedHandler 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 {
@Override
public 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;
}
@Override
public 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 {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder; // 注入BCryptPasswordEncoder
/**
* 用户注册
* @param user
*/
public void registerUser(User user) {
// 使用BCrypt加密密码
String encryptedPassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encryptedPassword);
userMapper.insert(user);
}
@Override
public 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;
@Data
public 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
接口测试: