基于Spring Security实现简单的登录验证(前后端分离 入门)

1,571 阅读2分钟

基于Spring Security实现简单的登录验证(前后端分离 入门)

一.登录的框架选择

在 Java 生态中,目前有 Spring SecurityApache Shiro 两个安全框架,可以完成认证和授权的功能。

Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。

Spring Security:是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。Spring Security 主要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与授权分离,并提供了扩展点。

按照现在常见的技术栈组合:

一般Shiro与SSM框架搭套

Spring Security与Spring Boot或Spring Cloud搭套

二.后端代码实现

代码主要是针对Spring Boot实现,具体的解释基本都已经写在代码中

1.pom文件

引入依赖

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--web核心依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>

        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>

2.Controller层及其对应的实体类

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.jlstest.springbootdemo.common.exception.CommonResultCode;
import com.jlstest.springbootdemo.common.response.BaseController;
import com.jlstest.springbootdemo.common.response.JlsTestResponse;
import com.jlstest.springbootdemo.model.LoginForm;
import com.jlstest.springbootdemo.service.UserService;
import com.jlstest.springbootdemo.util.JwtTokenUtil;

/**
 * @author J
 * @description:
 * @since 2023-06-29 15:12
 */
@RestController
@RequestMapping("/api/auth")
public class AuthController extends BaseController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    private UserService userService;

    @PostMapping("/login")
    public JlsTestResponse<?> login(@RequestBody LoginForm loginForm) {
        try {
            // 使用用户名和密码进行身份验证
            Authentication authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(loginForm.getUsername(), loginForm.getPassword()));

            // 在SecurityContext中设置认证信息
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // 生成JWT令牌
            UserDetails userDetails = userService.loadUserByUsername(loginForm.getUsername());
            String token = jwtTokenUtil.generateToken(userDetails);

            // 构建响应对象
            return sendSuccessData(token);
        } catch (AuthenticationException e) {
            // 处理身份验证失败的情况
            return JlsTestResponse.sendFailure(CommonResultCode.ERROR);
        }
    }
}
import com.jlstest.springbootdemo.aop.RateLimit;
import com.jlstest.springbootdemo.common.response.BaseController;
import com.jlstest.springbootdemo.common.response.JlsTestResponse;
import com.jlstest.springbootdemo.model.User;
import com.jlstest.springbootdemo.service.TestService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author J
 * @description:
 * @since 2023-03-22 19:09
 */
@RestController
@RequestMapping("/test")
public class TestController extends BaseController {

    @GetMapping("/test")
    @ResponseBody
    public JlsTestResponse<String> test() {
        return sendSuccess();
    }

}

上面代码中BaseController和JlsTestResponse为个人自定义的封装

具体实体类的实现:

import lombok.Data;

/**
 * @author J
 * @description:
 * @since 2023-06-29 15:21
 */
@Data
public class LoginForm {

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;
}

三.数据库语句以及对应的sql实现

1.sql脚本

CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(64) NOT NULL COMMENT '密码,加密存储',
  `phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
  `email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`) USING BTREE,
  UNIQUE KEY `phone` (`phone`) USING BTREE,
  UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';
insert  into `tb_user`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values
(37,'jls','$2a$10$9ZhDOBp.sRKat4l14ygu/.LscxrMUcDAfeVOEPiYwbcRkoB09gCmi','158xxxxxxx','xxxxxxx@gmail.com','2023-06-30 23:21:27','2023-06-30 23:21:29');

CREATE TABLE `tb_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
  `name` varchar(64) NOT NULL COMMENT '角色名称',
  `enname` varchar(64) NOT NULL COMMENT '角色英文名称',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';
insert  into `tb_role`(`id`,`parent_id`,`name`,`enname`,`description`,`created`,`updated`) values
(37,0,'超级管理员','jls',NULL,'2023-06-30 23:22:03','2023-06-30 23:22:05');


CREATE TABLE `tb_user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '用户 ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户角色表';
insert  into `tb_user_role`(`id`,`user_id`,`role_id`) values
(37,37,37);

CREATE TABLE `tb_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',
  `name` varchar(64) NOT NULL COMMENT '权限名称',
  `enname` varchar(64) NOT NULL COMMENT '权限英文名称',
  `url` varchar(255) NOT NULL COMMENT '授权路径',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='权限表';
insert  into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,`updated`) values
(37,0,'系统管理','System','/',NULL,'2023-06-30 23:22:54','2023-06-30 23:22:56'),
(38,37,'用户管理','SystemUser','/users/',NULL,'2023-06-30 23:25:31','2023-06-30 23:25:33'),
(39,38,'查看用户','SystemUserView','',NULL,'2023-06-30 15:30:30','2023-06-30 15:30:43'),
(40,38,'新增用户','SystemUserInsert','',NULL,'2023-06-30 15:30:31','2023-06-30 15:30:44'),
(41,38,'编辑用户','SystemUserUpdate','',NULL,'2023-06-30 15:30:32','2023-06-30 15:30:45'),
(42,38,'删除用户','SystemUserDelete','',NULL,'2023-06-30 15:30:48','2023-06-30 15:30:45'),
(44,37,'内容管理','SystemContent','/contents/',NULL,'2023-06-30 18:23:58','2023-06-30 18:24:00'),
(45,44,'查看内容','SystemContentView','/contents/view/**',NULL,'2023-06-30 23:49:39','2023-06-30 23:49:41'),
(46,44,'新增内容','SystemContentInsert','/contents/insert/**',NULL,'2023-06-30 23:51:00','2023-06-30 23:51:02'),
(47,44,'编辑内容','SystemContentUpdate','/contents/update/**',NULL,'2023-06-30 23:51:04','2023-06-30 23:51:06'),
(48,44,'删除内容','SystemContentDelete','/contents/delete/**',NULL,'2023-06-30 23:51:08','2023-06-30 23:51:10');

CREATE TABLE `tb_role_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
  `permission_id` bigint(20) NOT NULL COMMENT '权限 ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT='角色权限表';
insert  into `tb_role_permission`(`id`,`role_id`,`permission_id`) values
(37,37,37),
(38,37,38),
(39,37,39),
(40,37,40),
(41,37,41),
(42,37,42),
(43,37,44),
(44,37,45),
(45,37,46),
(46,37,47),
(47,37,48);

2.封装在代码中的sql实现 实体类:

import lombok.Data;

import java.io.Serializable;

/**
 * @author JLS
 * @description:
 * @since 2023-03-22 19:12
 */
@Data
public class User implements Serializable {

    public static final long serialVersionUID = 1L;

    /**
     * 主键id
     */
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 注册手机号
     */
    private String phone;

    /**
     * 注册邮箱
     */
    private String email;
}
import java.io.Serializable;

import lombok.Data;

/**
 * @author JLS
 * @description: 账户权限实体类
 * @since 2023-06-28 14:46
 */
@Data
public class Permission implements Serializable {
    public static final long serialVersionUID = 1L;

    /**
     * 主键id
     */
    private Long id;

    /**
     * 父权限
     */
    private Long parentId;

    /**
     * 权限名称
     */
    private String name;

    /**
     * 权限英文名称
     */
    private String enname;

    /**
     * 授权路径
     */
    private String url;

    /**
     * 备注
     */
    private String description;

}

3.dao层方法

package com.jlstest.springbootdemo.dao;

import com.jlstest.springbootdemo.model.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * @className: UserDao
 * @Description: TODO
 * @author: JLS
 * @date: 2023/6/28 14:39
 */
@Mapper
public interface UserDao {

    /**
     * 根据用户名获取用户信息
     */
    User getByUsername(String userName);

}
package com.jlstest.springbootdemo.dao;

import java.util.List;

import com.jlstest.springbootdemo.model.Permission;
import org.apache.ibatis.annotations.Mapper;

/**
 * @className: PermissionDao
 * @Description: TODO
 * @author: JLS
 * @date: 2023/6/28 14:49
 */
@Mapper
public interface PermissionDao {

    /**
     * 根据用户id查询用户权限
     */
    List<Permission> selectByUserId(Long userId);
}
<?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.jlstest.springbootdemo.dao.UserDao">

    <resultMap id="userResultMap" type="com.jlstest.springbootdemo.model.User">
        <result column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="phone" property="phone"/>
        <result column="email" property="email"/>
    </resultMap>

    <select id="getByUsername" resultMap="userResultMap">
        select id, username, password, phone, email
        from tb_user
        where username = #{userName}
    </select>

</mapper>
<?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.jlstest.springbootdemo.dao.PermissionDao">

    <resultMap id="permissionResultMap" type="com.jlstest.springbootdemo.model.Permission">
        <result column="id" property="id"/>
        <result column="parent_id" property="parentId"/>
        <result column="name" property="name"/>
        <result column="enname" property="enname"/>
        <result column="url" property="url"/>
        <result column="description" property="description"/>
    </resultMap>

    <select id="selectByUserId" resultMap="permissionResultMap">
        select tp.id, tp.parent_id, tp.name, tp.enname, tp.url, tp.description
        from tb_permission tp
                 left join tb_role_permission tr on tp.id = tr.permission_id
        where tr.role_id = (select role_id from tb_user_role where user_id = #{userId})
    </select>
</mapper>

四.重写UserDetailsService接口的方法

里面的核心方法是 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

用于加载用户信息,如果不进行重写,默认情况下,UserDetailsServiceAutoConfiguration自动化配置类,会创建一个内存级别InMemoryUserDetailsManager对象,提供认证的用户信息。

1.重写UserDetailsService

import org.springframework.security.core.userdetails.UserDetailsService;

import com.jlstest.springbootdemo.model.User;

/**
 * @className: UserService
 * @Description: TODO
 * @author: JLS
 * @date: 2023/6/29 15:08
 */
public interface UserService extends UserDetailsService {

    /**
     * 获取用户信息
     * 
     * @param username
     *            用户名
     * @return 用户信息
     */
    User getUserByUsername(String username);
}
import java.util.ArrayList;
import java.util.List;

import javax.annotation.Resource;

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.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import com.jlstest.springbootdemo.dao.PermissionDao;
import com.jlstest.springbootdemo.dao.UserDao;
import com.jlstest.springbootdemo.model.Permission;
import com.jlstest.springbootdemo.model.User;
import com.jlstest.springbootdemo.service.UserService;

/**
 * @author JLS
 * @description:
 * @since 2023-06-29 15:03
 */
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserDao userDao;

    @Resource
    private PermissionDao permissionDao;

    /**
     * 获取用户信息
     *
     * @param username
     *            用户名
     * @return 用户信息
     */
    @Override
    public User getUserByUsername(String username) {
        return userDao.getByUsername(username);
    }

    /**
     * Locates the user based on the username. In the actual implementation, the search may possibly
     * be case sensitive, or case insensitive depending on how the implementation instance is
     * configured. In this case, the <code>UserDetails</code> object that comes back may have a
     * username that is of a different case than what was actually requested..
     *
     * @param username
     *            the username identifying the user whose data is required.
     * @return a fully populated user record (never <code>null</code>)
     * @throws UsernameNotFoundException
     *             if the user could not be found or the user has no GrantedAuthority
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 获取用户信息
        User user = getUserByUsername(username);
        List<GrantedAuthority> authorities = new ArrayList<>();

        if (user != null) {
            // 获取当前用户的权限
            List<Permission> permissions = permissionDao.selectByUserId(user.getId());
            // 设置权限
            permissions.forEach(permission -> {
                if (permission != null && !StringUtils.isEmpty(permission.getEnname())) {
                    GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getEnname());
                    authorities.add(grantedAuthority);
                }
            });
            // 封装成UserDetails的实现类
            return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
        } else {
            throw new UsernameNotFoundException("用户名不存在");
        }
    }
}

通过上面的重写,实现了数据库配置用户信息,进行用户信息管理

五.继承重写WebSecurityConfigurerAdapter

1.SecurityConfig实现:

import java.util.Arrays;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.jlstest.springbootdemo.service.UserService;

/**
 * @author JLS
 * @description:
 * @since 2023-06-29 17:10
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Resource
    private JwtRequestFilter jwtRequestFilter;

//    /**
//     * 配置 Spring Security 忽略对某些特定请求的安全验证。一般用于测试
//     */
//    @Override
//    public void configure(WebSecurity web) {
//
//        // 表示忽略对 HTTP OPTIONS 请求方法的验证,并且对所有路径 /** 都生效。
//        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**").antMatchers("/test/test");
//    }

    /**
     * 配置 Spring Security 的安全策略和过滤器链的方法。通过重写该方法,你可以自定义对不同请求的访问控制、认证配置、异常处理
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭csrf防护 csrf跨站点请求 可以通过token来避免跨站点请求的攻击 csrf跨站请求伪造
        // (如果前后端都注册在同一个nginx上,则不存在跨站点请求的攻击问题,毕竟是同源的,所以在生产环境上可以不用关闭csrf的保护)
        http.csrf().disable()
                // 表示对于 /api/auth/login 路径的请求,允许所有用户进行访问,即不需要进行身份验证和授权即可访问该路径。
                .authorizeRequests().antMatchers("/api/auth/login").permitAll()
                // 表示对于其他所有请求,要求用户进行身份验证才能访问
                .anyRequest().authenticated();

        // 配置身份验证异常的处理类,使得在身份验证失败时能够自定义处理逻辑,并返回适当的响应信息。
        http.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint);

        // 在无状态会话策略下,服务器不会创建或使用会话来存储用户状态,每次请求都需要进行身份验证。
        // 这种策略适用于前后端分离的应用或无需维护用户会话状态的场景,因为它不依赖于会话来存储用户信息或认证状态,而是通过每个请求中的令牌或其他身份验证信息来进行身份验证和授权。
        // 配置会话创建策略为无状态会话,从而使得系统不依赖于会话来存储用户状态,提高系统的可伸缩性和安全性。
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 这段代码的作用是将自定义的 JWT 请求过滤器添加到 Spring Security 过滤器链中,
        // 并在处理用户名和密码的身份验证之前对 JWT 进行验证和解析。这样可以实现基于 JWT 的身份验证机制。
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        // 开启cors配置,主要用于解决跨域问题,并结合 corsConfigurationSource()使用
        http.cors();
    }

    // 配置 Spring Security 的身份验证管理器。并指定身份验证的方式和提供者。
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // .userDetailsService(userService) 指定了用于加载用户信息的自定义 UserDetailsService 接口的实现类 userService。
        // .authenticationProvider(authenticationProvider()) 指定了身份验证的提供者。
        // authenticationProvider() 方法返回一个 AuthenticationProvider 的实例,其中包含了自定义的身份验证逻辑和处理方式。
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder()).and().authenticationProvider(authenticationProvider());
    }

    /**
     * 定义密码加密方式,一般推荐使用BCryptPasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 用于验证用户信息,其中的userService则是自定义的用户信息获取
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }

    /**
     * 处理用户信息的核心接口, 负责管理认证过程,包括验证用户的身份、加载用户的权限信息, 最后结果返回,验证后的用户身份信息和相关的权限信息
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // 处理跨域问题,一般只有在本地测试,使用浏览器访问时才会出现跨域问题,如果是都部署到nginx上则没有跨域的问题
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080")); // 添加允许跨域的源
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

}

由于我都是本地运行代码,没有经过其他代理,所以需要关闭csrf 即http.csrf().disable()。具体的在代码中有写

2.JwtTokenUtil的封装

JwtTokenUtil 是一个工具类,用于操作和处理 JSON Web Token (JWT)。JWT 是一种用于身份验证和授权的开放标准,通过在服务器和客户端之间传递、验证和存储 token 来实现安全通信。

import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

/**
 * @author JLS
 * @description:
 * @since 2023-06-29 15:32
 */
@Component
public class JwtTokenUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    // 从令牌中获取用户名
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    // 从令牌中获取过期时间
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    // 根据用户信息生成令牌
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    // 根据传入的声明生成令牌
    private String createToken(Map<String, Object> claims, String subject) {
        Date now = new Date();
        Date expirationDate = new Date(now.getTime() + expiration * 1000);

        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(now).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret)
                // .signWith(key, SignatureAlgorithm.HS512)
                .compact();
    }

    // 验证令牌是否有效
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    // 验证令牌是否过期
    private boolean isTokenExpired(String token) {
        Date expirationDate = getExpirationDateFromToken(token);
        return expirationDate.before(new Date());
    }

    // 从令牌中获取声明信息
    private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        return claimsResolver.apply(claims);
    }

    // 创建JWT密钥,用于生成配置文件中用的密钥 (仅仅测试中适合使用)
    public static void main(String[] args) {
        // Generate a secure random JWT secret key for HS512 algorithm
        Key key = Keys.secretKeyFor(SignatureAlgorithm.HS512);

        // Convert the key to a Base64-encoded string
        String base64Key = Base64.getEncoder().encodeToString(key.getEncoded());

        System.out.println(base64Key);

    }
}

3.自定义的过滤器方法,用于实现对请求进行过滤和处理的逻辑

import com.jlstest.springbootdemo.service.UserService;
import com.jlstest.springbootdemo.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
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;

/**
 * @author JLS
 * @description:
 * @since 2023-06-29 17:12
 */
@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserService userService;

    /**
     * 自定义的过滤器方法,用于实现对请求进行过滤和处理的逻辑
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        // 从请求中获取 Authorization 请求头
        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;

        // 从 Authorization 请求头中获取 JWT Token 从 JWT Token 中获取用户名
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwtToken = authorizationHeader.substring(7);
            // 使用 JWT Token 解析工具 jwtTokenUtil 解析出用户名
            username = jwtTokenUtil.getUsernameFromToken(jwtToken);
        }

        // 如果用户名不为空且当前 Security 上下文中没有认证信息,则加载用户详细信息并验证 JWT Token 的有效性
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // 从数据库中获取用户信息
            UserDetails userDetails = userService.loadUserByUsername(username);

            // 验证 JWT Token 是否有效,如果有效则在 SecurityContextHolder 中进行设置
            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null,
                        userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 将请求的身份认证信息设置到 Security 上下文中
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }

        // 继续执行过滤器链中的下一个过滤器
        chain.doFilter(request, response);
    }
}

4.自定义的身份验证入口点类

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author JLS
 * @description: 自定义的身份验证入口点类
 * @since 2023-06-29 17:14
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    /**
     * commence 方法是 AuthenticationEntryPoint 接口中的一个方法,用于处理未经授权的请求的入口点。
     * 当用户尝试访问需要认证的资源而未提供有效的身份验证凭据时,会触发 commence 方法
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

六.配置文件

server:
  port: 8081
spring:
  #数据库连接配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456

#mybatis的相关配置
mybatis:
  #mapper配置文件
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.zhg.demo.mybatis.entity
  #开启驼峰命名
  configuration:
    map-underscore-to-camel-case: true

jwt:
  secret: CD1GkiDu8sdCi02zwI/eEWJ3651OYazKLwnXPo1CVxYAJhVyH82BCTyF0spZsM1/hfVk2pVEmfNzMBZAv/Mwug==
  expiration: 86400

七.前端测试界面

由于不会前端,所以只能写一个简单的测试界面用于登录测试

server.js文件:

const http = require('http');
const fs = require('fs');
const path = require('path');

const hostname = 'localhost';
const port = 8080;

const server = http.createServer((req, res) => {
    const filePath = path.join(__dirname, req.url === '/' ? '/index.html' : req.url);
    const fileExtension = path.extname(filePath);
    const contentType = {
        '.html': 'text/html',
        '.css': 'text/css',
        '.js': 'text/javascript',
    }[fileExtension] || 'application/octet-stream';

    fs.readFile(filePath, (error, content) => {
        if (error) {
            if (error.code === 'ENOENT') {
                res.writeHead(404);
                res.end('File not found');
            } else {
                res.writeHead(500);
                res.end('Internal server error');
            }
        } else {
            res.writeHead(200, { 'Content-Type': contentType });
            res.end(content, 'utf-8');
        }
    });
});

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});

index.html文件:

<!DOCTYPE html>
<html>

<head>
    <title>Login Page</title>
    <style>
        body {
            font-family: Arial, sans-serif;
        }

        .container {
            max-width: 300px;
            margin: 0 auto;
            padding: 20px;
            border: 1px solid #ccc;
            border-radius: 4px;
            background-color: #f2f2f2;
        }

        .form-group {
            margin-bottom: 10px;
        }

        .form-group label {
            display: block;
            margin-bottom: 5px;
        }

        .form-group input {
            width: 100%;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }

        .form-group button {
            width: 100%;
            padding: 8px;
            border: none;
            border-radius: 4px;
            background-color: #4CAF50;
            color: #fff;
            cursor: pointer;
        }
    </style>
</head>

<body>
    <div class="container">
        <h2>Login</h2>
        <form id="login-form">
            <div class="form-group">
                <label for="username">Username:</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div class="form-group">
                <label for="password">Password:</label>
                <input type="password" id="password" name="password" required>
            </div>
            <div class="form-group">
                <button type="submit">Login</button>
            </div>
        </form>
    </div>

    <script>
        document.getElementById('login-form').addEventListener('submit', function (event) {
            event.preventDefault();

            // Get the form data
            var username = document.getElementById('username').value;
            var password = document.getElementById('password').value;

            // Send the login request to the backend API
            var xhr = new XMLHttpRequest();
            xhr.open('POST', 'http://localhost:8081/api/auth/login');
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.onload = function () {
                if (xhr.status === 200) {
                    // // Successful login, redirect to dashboard or desired page
                    // window.location.href = '/success.html';

                    // Successful login, extract the token from the response
                    var response = JSON.parse(xhr.responseText);
                    var token = response.data;

                    // Include the token in the subsequent request to /test/testd
                    var testXhr = new XMLHttpRequest();
                    testXhr.open('GET', 'http://localhost:8081/test/test');
                    testXhr.setRequestHeader('Authorization', 'Bearer ' + token);
                    testXhr.onload = function () {
                        if (testXhr.status === 200) {
                            // Successful request to /test/testd
                            var responseData = JSON.parse(testXhr.responseText);
                            alert(responseData.message);
                            
                            // Handle the response data as needed
                        } else {
                            // Failed request to /test/testd
                            alert('Failed to access /test/test. Please try again.');
                        }
                    };
                    testXhr.send();

                } else {
                    // Failed login, display error message
                    alert('Invalid username or password. Please try again.');
                }
            };
            xhr.send(JSON.stringify({ username: username, password: password }));
        });
    </script>
</body>

</html>

弄完后,打开命令窗口打开 node server.js

image-20230701165637032

运行后打开界面登录即可:

image-20230701165730923

登录成功后展示:

image-20230701165926598