5、幽络源微服务项目实战:后端Security+JWT实现登录认证接口开发

5、幽络源微服务项目实战:后端Security+JWT实现登录认证接口开发

前言

上节教程幽络源创建了后端的公共模块和多租户模块, 并且将公共模块引入到了多租户模块,这节我们来利用SpringSecurity+JWT实现后端的登录认证功能。

表的调整

在第三节教程中,我们初步分析了多租户的表的设计,但是考虑到User是mysql的关键字,因此这里幽络源决定将表都加上前缀”tsb_”,字段和属性都不变化。

导入数据表

在本文最后会给出本节教程的源码与SQL脚本,如图,有两张表,一张是结构表,一张是数据表,先后导入即可,我这里使用的MySQL为5.7的版本。

740a1a84-0711-44b3-b9a4-5ac696b7ab56

注意这里创建数据库时字符集选择utf8mb4,如图

d696ee55-a47d-4186-a527-19c03eb7453b

依赖添加

在公共模块common中添加测试的依赖,后面我们会做加密解密测试JWT的生成和解析测试。如下

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

在多租户模块tenant中添加 security 和 jwt 的依赖,如下

<!-- 安全控制 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jjwt-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

yaml配置添加

在多租户模块tenant中添加MySQL的配置,以及MyBatisPlus,如下

server:
  port: 8001

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/marmot_inspection?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 159357
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

创建统一响应类

前端要请求,我们后端就搞个统一的响应类用于整个项目的结果响应,这个响应类这里幽络源放在了common模块的utils目录下。

package com.common.utils;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResResult<T> {
    private int code;
    private String message;
    private T data;
    public ResResult(T data) {
        this.code = 1;
        this.message = "success";
        this.data = data;
    }
    public ResResult( String message) {
        this.code = 0;
        this.message = message;
        this.data = null;
    }
    public static <T> ResResult<T> success(T data) {
        return new ResResult<>(data);
    }
    public static <T> ResResult<T> fail(String message) {
        return new ResResult<>(message);
    }

}

如图

4af1a409-9b15-4a97-9dac-be5926133eeb

创建多租户模块的包结构

如图,创建基本的包结构,然后将首先将实体类和Mapper接口对应着数据表创建好,实体类和Mapper这里不赘述。

73d1eb0d-75e7-4f09-b75a-70bee6f2ba00

额外的类

JWT工具类,这个工具类的主要作用目前就是最简单的生成jwt与解析jwt

package com.tenant.utils;

import com.tenant.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtils {

    private static String signKey = "abcdefghijklmnopqrstuvwxyzqwertyuiopasdfghjklzxcvbnm";
    private static Long expire = 43200000L;

    /**
     * 生成JWT令牌
     * @param claims JWT第二部分负载 payload 中存储的内容
     * @return
     */
    public static String generateJwt(Map<String, Object> claims){
        String jwt = Jwts.builder()
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
        return jwt;
    }

    /**
     * 解析JWT令牌
     * @param jwt JWT令牌
     * @return JWT第二部分负载 payload 中存储的内容
     */
    public static Claims parseJWT(String jwt){
        Claims claims = Jwts.parser()
                .setSigningKey(signKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

请求参数DTO,接受前端登录接口的请求参数的数据传输类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {
    private String username;
    private String password;
}

三个Security的核心的配置

UserDetailObject,这个类主要是实现了Security自带的UserDetails对象,因为在使用Security必须得用他,但我们又期望用上自己的User实体类,因此在里面加入了我们的User属性,如下

package com.tenant.security;

import com.tenant.entity.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;
//2
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailObject implements UserDetails {

    private User user;

    /**
     * @return 权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList();
    }

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

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

    /**
     * 是否不过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 是否不锁用户
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 凭着是否不过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 账户是否可用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserDetailServiceImpl,这个实现类主要是实现Security自带的UserDetailsService,重写其用户的认证,重写为在用MyBatisPlus在数据库去查找用户,如下

package com.tenant.security;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.tenant.entity.User;
import com.tenant.mapper.UserMapper;
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 javax.annotation.Resource;
import java.util.Objects;

//1
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    /**
     * 实现自定义认证
     * authenticationManager.authenticate内部在调用这里
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户新消息
        LambdaQueryWrapper<User> eq = new LambdaQueryWrapper<User>()
                .eq(User::getUsername, username);
        User user = userMapper.selectOne(eq);
        if (!Objects.isNull(user)){
            UserDetailObject userDetailObject = new UserDetailObject(user);
            return userDetailObject;
        }else{
            throw new UsernameNotFoundException("无此用户");
        }
    }

}

ExtendAdapter,这是一个配置类,主要集成了Security自带的WebSecurityConfigurerAdapter,在这里面我们定义了加密方式,获取Security的AuthenticationManager用于我们自己的登录方法,还有配置HTTP 请求的安全规则,如下

package com.tenant.security;

import com.tenant.filter.TokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

//3
@Configuration
public class ExtendAdapter extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * security的认证管理器
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Resource
    private TokenFilter tokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .cors().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/auth/login").permitAll()
                .anyRequest().authenticated();
        //添加我们自己的过滤器,在登录前就加上token过滤器
        http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

流程简述

这节所实现的登录功能主要流程为如下,可以结合幽络源提供的源码体会。

前端通过POST方式携带用户名username和密码password请求/auth/login接口,接口调用loginService的login方法,方法中主要通过Security的authenticationManager来进行认证,当authenticationManager调用authenticate时,会来到我的实现了UserDetailsService的UserDetailServiceImpl实现类中重写的loadUserByUsername方法,loadUserByUsername方法中我们自己通过MyBatisPlus去查询数据库看是否有此用户,若有此用户则会将用户的UserId通过JWT工具类进行生成token返回给前端,否则响应用户名或密码错误。

密码的加密解密和JWT的生成与解析

关于密码的加密解密,以及JWT的生成和解析在幽络所提供的源码中都有对应的测试可直接使用,如图

af925048-4e79-4bde-8f47-6b2b4e8ccc20

登录的测试

这里幽络源所提供的SQL脚本中所有用户密码都为“youluoyuan.com”,如果要修改密码可通过PasswordEncoder的测试方法去生成自己所需要的密码让后放入数据库中。

密码错误测试

如图首先测试错误的密码,如图,当密码错误时,实际上下面这行代码会抛出一个BadCredentialsException异常,并且异常的消息为“用户名或密码错误”。

authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);

702017dd-6f79-4b56-872d-84ae5ce2e0c1

密码正确测试

如图,当用户名和密码都正确时,这里则会将用户的信息一并生成JWT响应给前端。

3d93efc4-e25e-4fae-8fda-c0e8db3fd365

源码与SQL

https://pan.quark.cn/s/c14f1ee59862

结语

本章主要为整合SpringSecurity来做了一个登录的认证,后续我们还要加入授权的功能。如上为幽络源的5、幽络源微服务项目实战:后端Security+JWT实现登录认证接口开发教程,如有疑问或对微服务感兴趣可加入我们的QQ群询问与交流:307531422

THE END
喜欢就支持一下吧
分享