前言
上节教程幽络源创建了后端的公共模块和多租户模块, 并且将公共模块引入到了多租户模块,这节我们来利用SpringSecurity+JWT实现后端的登录认证功能。
表的调整
在第三节教程中,我们初步分析了多租户的表的设计,但是考虑到User是mysql的关键字,因此这里幽络源决定将表都加上前缀”tsb_”,字段和属性都不变化。
导入数据表
在本文最后会给出本节教程的源码与SQL脚本,如图,有两张表,一张是结构表,一张是数据表,先后导入即可,我这里使用的MySQL为5.7的版本。

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

依赖添加
在公共模块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);
}
}
如图

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

额外的类
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的生成和解析在幽络所提供的源码中都有对应的测试可直接使用,如图

登录的测试
这里幽络源所提供的SQL脚本中所有用户密码都为“youluoyuan.com”,如果要修改密码可通过PasswordEncoder的测试方法去生成自己所需要的密码让后放入数据库中。
密码错误测试
如图首先测试错误的密码,如图,当密码错误时,实际上下面这行代码会抛出一个BadCredentialsException异常,并且异常的消息为“用户名或密码错误”。
authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);

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

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

