SpringSecurity前后端分离登录认证

2年前 (2022) 程序员胖胖胖虎阿
319 0 0

文章总结自三更草堂SpringSecurity框架教程,个人认为是B站最好用的Security+JWT讲解。

1. 项目搭建

1.1 新建SpringBoot项目

SpringBoot使用的是2.7.0版本
依赖:

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

创建一个HelloController

@RestController
@RequestMapping("/hello")
public class HelloController {

    @GetMapping
    public String hello(){
        return "hello";
    }

}

访问测试:可以看到可以访问成功

SpringSecurity前后端分离登录认证

1.2 引入SpringSecurity依赖

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

重启项目之后发现localhost:8088/hello接口无法再次进行访问,取而代之的是Security的登录页。

SpringSecurity前后端分离登录认证
此时的默认用户名是user,密码会在控制台输出。

2. 认证

2.1 登录流程

SpringSecurity前后端分离登录认证

2.2 security认证流程

本质使用的是过滤器链,内部提供各种功能的过滤器。

  • 核心过滤器
    SpringSecurity前后端分离登录认证

    1. UsernamePasswordAuthenticationFilter:处理登录页中的登录请求;
    2. ExceptionTranslationFilter:处理过滤器链中的异常;
    3. FilterSecurityInterceptor:权限校验过滤器。
  • 1.2案例认证流程:
    SpringSecurity前后端分离登录认证

    1. Authentication:实现类,表示当前访问系统的用户,封装了相关用户信息;
    2. AuthenticationManager:定义认证Authentication方法;
    3. UserDetailsService:加载用户特定数据的核心接口;包含一个根据用户名查询用户信息的方法。
    4. UserDetails:提供核心用户信息,通过UserDetailsService根据用户名获取处理的用户信息封装成UserDetailsService对象返回,然后将信息封装到Authentication对象中。

2.3 实现思路

使用自己定义的接口去掉哟SpringSecurity中封装的类。

  • 登录流程:
    SpringSecurity前后端分离登录认证

  • 校验过程:
    SpringSecurity前后端分离登录认证

  • 引入redis:如果认证之后害需要通过JWT中的userid对数据库进行查询,消耗太大,可以存储入到redis中。

2.4 实现方案

  • 登录
    1. 自定义登录接口:调用ProviderManager的方法进行认证;认证通过生成JWT;将用户信息存入redis;
    2. 自定义UserDetailsService:实现数据库查询。
  • 校验
    1. 定义JWT过滤器:获取token,解析token,获取userid,去redis中获取用户信息;将用户信息存入SecurityContextHolder

2.5 简单实现

  • 创建数据库
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

实体类:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SysUser implements Serializable {

    @TableId
    private Long id;

    private String username;

    private String password;

    private Character status;

    private Integer delFlag;

}
  • 引入数据库相关依赖
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>
  • 配置mapper
@Component
public interface SysUserMapper extends BaseMapper<SysUser> {

}

添加注解扫描

@SpringBootApplication
@MapperScan("com.jm.springsecurity.mapper")
public class SpringSecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityApplication.class, args);
    }

}

  • 构建登录接口:实现security中的类,自己实现登录逻辑UserDetailsService
@Service
public class SysUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 查询用户信息
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username",username);
        SysUser user = userMapper.selectOne(queryWrapper);
        if (Objects.isNull(user)){
            throw new RuntimeException("用户不存在");
        }

        return new LoginUser(user);
    }


}

  • 封装登录返回实体类:实现UserDetails 接口自定义返回逻辑。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private SysUser user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

  • 登录测试: 登录失败,原因是security拥有自己的密码校验,需要重写。
    SpringSecurity前后端分离登录认证

    新建SecurityConfig BCryptPasswordEncoder注入到spring容器中,security就会自动使用来替换掉原有的密码校验方式。

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    }
    

    加密:针对密码进行加密之后,将加密后的密码存入数据库,登录时会自动走当前加密方式,不需要再做多余处理,即可登录成功。

      	@Autowired
        private PasswordEncoder passwordEncoder;
    
     System.out.println(passwordEncoder.encode("1234"));
    

2.6 JWT使用

  • 工具类
/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
    
    //设置秘钥明文
    public static final String JWT_KEY = "jmyy";

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("sg")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    public static void main(String[] args) throws Exception {

        // 加密
        String jwt = createJWT("2123");
        System.out.println(jwt);

        // 解密
        Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMzU5Yzk3ZGFmNDE0MzEzOTMyZjYxMDZkNWYyNzg4YSIsInN1YiI6IjIxMjMiLCJpc3MiOiJzZyIsImlhdCI6MTY1NDQ5ODM4MCwiZXhwIjoxNjU0NTAxOTgwfQ.hAFpmr6u_AtlMEs9SqS9TT9yuzbdSDDNsuWMLWmKIgU");
        String subject = claims.getSubject();
        System.out.println(subject);

    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }


}

使用JWT工具类来实现加密和解密。

2.7 自定义登录接口

  • 新增接口:
@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/login")
    public Result login(@RequestBody SysUser user){
        return new Result(200,"登陆成功",loginService.login(user));
    }

}
  • Security放行登录接口:
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
//                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
	}
  • 配置redis:
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    password: 123456
  • 认证逻辑

    1. AuthenticationManager 注入到容器中
        @Bean
        @Override
        protected AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
    1. 登录接口实现:
    /**
     * @Description
     * @date 2022/6/6 15:02
     */
    public interface LoginService {
    
        TokenVO login(SysUser user);
    
    }
    
    @Service
    public class LoginServiceImpl implements LoginService {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private RedisCache redisCache;
    
        @Override
        public TokenVO login(SysUser user) {
    
            // 认证
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(
                            user.getUsername(),
                            user.getPassword());
            Authentication authenticate = authenticationManager.authenticate(authenticationToken);
    
            // 认证失败
            if (Objects.isNull(authenticate)){
                throw new RuntimeException("用户名或密码错误");
            }
    
            // 认证成功
            LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
            String userId = loginUser.getUser().getId().toString();
    
            // 根据用户id生成jwt
            String jwt = JwtUtil.createJWT(userId);
    
            // 将用户信息存入redis
            redisCache.setCacheObject("login:"+userId,loginUser);
    
            return new TokenVO(jwt);
        }
    
    }
    
    1. 返回vo
    @Data
    @AllArgsConstructor
    public class TokenVO {
    
        private String token;
    
    }
    

2.8 jwt过滤器

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("token");
        if (StringUtils.hasText(token)) {
            //直接放行 让后面原生的 security 去拦截
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token error");
        }

        // 从redis 获取用户信息
        String redisKey = "login:" + userId;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)){
            throw new RuntimeException("用户为登录");
        }

        // 将用户信息存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(
                        loginUser,
                        null,
                        null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 放行
        filterChain.doFilter(request, response);
    }

}

  • 配置jwt过滤器:将当前过滤器放到securityUsernamePasswordAuthenticationFilter前面。修改SecurityConfig

	    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
//                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        // 添加过滤器到某个过滤器前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        }

2.9 退出登录

    @Override
    public Result logout() {
        // 获取SecurityContextHolder中用户id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userId = loginUser.getUser().getId();
        // 删除redis中值
        redisCache.deleteObject("login:"+userId);
        return new Result(200,"注销成功");
    }
    @GetMapping("/logout")
    public Result logout(){
        return loginService.logout();
    }

版权声明:程序员胖胖胖虎阿 发表于 2022年11月7日 上午5:08。
转载请注明:SpringSecurity前后端分离登录认证 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...