【Java】在 Spring Boot 中集成 Spring Security + JWT 实现基于 Token 的身份认证

发布于:2025-06-12 ⋅ 阅读:(19) ⋅ 点赞:(0)

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),UserMapperRoleMapper

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 接口。

AuthenticationEntryPointExceptionTranslationFilter 来启动身份认证方案。它是一个入口点,用于检查用户是否已通过身份认证,如果用户已经认证,则登录该用户,否则抛出异常(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,可确保每个请求只执行一次过滤器。
  • 构造函数需要两个依赖:JwtTokenProviderUserDetailsService,它们是通过 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);
        }
    }
}

其中,ApiResultUserRegisterRequestDTOJWTAuthResponse分别如下:

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数据库中新建一个数据库,并新增四张数据表userrolepermissionrole_permission

注册功能register接口测试:

在这里插入图片描述

登录功能login接口测试:

在这里插入图片描述
登出功能logout接口测试:

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到