书接上篇:响应式项目(RxJS+Vue.js+Spring)大决战(5):主页的实现(前端视图模块)
6 用户登录
6.1 功能需求及界面设计
用户登录模块用于学生和教师的日常登录处理,功能需求主要包括:(1)构建用户登录主界面;(2)实现登录业务处理;(3)登录成功后,生成JWT令牌以备其他功能验证使用;(4)登录试错的屏幕锁定。用户登录界面如下图所示。
正常登录界面
锁定登录界面
6.2 相关数据表
6.2.1 表结构与SQL语句
与用户登录有关的数据表是users表,其字段构成如下。
字段名 | 数据类型 | 长度 | 是否为空 | 主键 | 默认值 | 含义 |
username | character varying | 33 | 否 | 是 | 用户名 | |
password | character varying | 60 | 否 | 密码 | ||
logo | character varying | 33 | 是 | image/loginer.png | 用户图标 | |
role | character varying | 20 | 否 | 用户角色 | ||
| character varying | 30 | 是 | 邮箱 |
创建users表的SQL语句如下:
CREATE TABLE public.users (
username character varying(33) NOT NULL,
password character varying(60) NOT NULL,
logo character varying(33) DEFAULT 'image/loginer.png'::character varying,
role character varying(20) NOT NULL,
email character varying(30),
CONSTRAINT users_pkey PRIMARY KEY (username)
);
ALTER TABLE public.users OWNER TO admin;
COMMENT ON TABLE public.users IS '用户表';
6.2.2 构建配置和实体类
(1)修改app-server/app-domain模块的build.gradle。记得重载更改。
plugins {
id 'server.common'
}
(2)新建实体类Users
在app-server/app-domain/src/main/java下创建包com.tams.entity,新建实体类Users.java:
package com.tams.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users implements Serializable {
@Id
private String username; //用户名
private String password; //密码
private String logo; //头像
private String role; //角色
private String email; //邮箱
}
6.3 使用JWT令牌
6.3.1 JWT令牌简介
JWT是JSON Web Token的简称,基于工业化标准RFC7519,是一个开放的标准协议。JWT能够以JSON格式安全地传输用户登录状态,也为跨域问题提供了支持。
JWT由三个部分组成:Header、Payload、Signature。
(1)Header头部
描述关于该 JWT 的最基本信息:
{
"alg": "HS256",
"typ": "JWT"
}
alg定义了使用的算法,typ定义了类型。该JSON对象需要转换为经过Base64URL编码的字符串(数据①):
ewoJImFsZyI6ICJIUzI1NiIsCgkidHlwIjogIkpXVCIKfQ
(2)Payload载荷
JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。JWT指定了七个默认属性以供选择使用,并允许自定义属性:
- iss:签发人。
- exp(expires):失效时间。
- sub:主题。
- aud:受众。
- nbf(not before):在此时间前不可用。
- iat(issued at):签发时间。
- jti(JWT id):标识ID。
这些属性并不是每个都必须定义。例如:
{
"sub": "JWT",
"iss": "http://www.hust.edu.cn",
"iat": 1451888119,
"exp": 1454516119,
"jti": "hust0102038",
"aud": "EM",
"myname": "ccgg"
}
其中myname是自定义属性。Payload也需要经过Base64URL进行编码,生成一个新的字符串(数据②):
ewogICAgInN1YiI6ICJKV1QiLAogICAgImlzcyI6ICJodHRwOi8vd3d3Lmh1c3QuZWR1LmNuIiwKICAgICJpYXQiOiAxNDUxODg4MTE5LAogICAgImV4cCI6IDE0NTQ1MTYxMTksCiAgICAianRpIjogImh1c3QwMTAyMDM4IiwKICAgICJhdWQiOiAiRU0iLAogICAgIm15bmFtZSI6ICJjY2dnIgp9
(3)Signature签名
先将上面①、②两部分数据组合在一起,中间以“.”隔开,即:①.②。然后,通过指定的密钥及算法,对其进行运算生成签名。例如,如果使用密钥007、SHA-256加密算法对①.②进行加密,则最后得到签名(数据③):
T5ucJ3Lzf9u3t5mdcdBTaNwS1UG4QVJL3HElOfEvnwA
将这三部分数据组合在一起,每个部分之间用"."分隔,即“①.②.③”的形式,组合成一个字符串,就构成JWT令牌。
6.3.2 使用Nimbus JOSE+JWT处理令牌
Nimbus JOSE + JWT是一个流行的、免费开源、功能强大的Java库,主要用于JWT的各种处理。Nimbus JOSE + JWT涵盖所有标准签名和加密算法,官网地址:https://connect2id.com/products/nimbus-jose-jwt。Nimbus JOSE + JWT生成JWT令牌的主要方法简要列举如下:
- JWSHeader.Builder(JWSAlgorithm):利用JWSAlgorithm算法生成头部数据header。
- JWTClaimsSet.Builder():生成JWT的主体内容payload。
- SignedJWT(header, payload):使用header、payload得到签名。
- RSASSAVerifier(publicKey):利用公钥publicKey验证JWT的有效性。
- JWEObject.encrypt():使用公钥加密JWT。
- JWEObject.parse(token):将令牌解析为JWEObject对象。
- JWEObject.decrypt(RSAEncrypter):将加密后的JWT解密。
6.3.3 创建Assitant令牌生成和校验工具类
我们来编写一个专门用于令牌生成和校验的工具类Assitant.java。这个工具类为项目各模块服务,因此放置在app-server/app-util模块下。Assitant.java类提供3个方法:
- getToken(String username):根据登录用户的用户名,生成令牌。
- verifyToken(String token, String username):根据用户名、令牌,校验令牌的有效性。
- passwordEncoder():对密码进行加密。这里采用Spring Security 提供的高强度对称加密方法BCryptPasswordEncoder()。BCryptPasswordEncoder将明文进行哈希处理,生成密文。其特点是:同样的明文,多次加密后的密文并不相同。例如密码123456,加密后的密文可以是:
$2a$10$6.sbp0/YHfa2MEJPkn9j6ej1o1XzTi.UiEVHnvymF7ZjQfrpetdBK
也可以是:
$2a$10$dDWTN2xum3CgO7eGTtej9.XxLe2bYMCzx7xVNplZL6pgUTsBdwtHy
(1)修改app-util/build.gradle构建脚本
plugins {
id 'server.common'
id 'java'
}
dependencies {
implementation libs.jose.jwt //请参阅tams/gradle/libs.versions.toml文件
}
记得务必重载更改,不然下面创建Assitant.java工具类时,会由于无法导入nimbus.jose中的相应类而出错!
(2)新建Assitant.java工具类
在app-util模块的src/main/java文件夹下新建包com.tams,在该包下创建Assitant.java,代码如下:
package com.tams;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSADecrypter;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Random;
@Component
public class Assistant {
private static final RSAKey rsaJWK; //公钥
public static RSAKey publicKey; //RSA密钥对象
static {
try {
rsaJWK = new RSAKeyGenerator(2048) //设置密钥长度为2048位
//随机生成密钥的ID号
.keyID(String.valueOf(new Random().nextLong()))
.generate();
publicKey = rsaJWK.toPublicJWK(); //生成公钥
} catch (JOSEException e) {
throw new RuntimeException(e);
}
}
public String getToken(String username) {
String jweString; //JWT字符串
try {
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID(rsaJWK.getKeyID())
.build(); //生成头部Header
Date today = new Date();
JWTClaimsSet payload = new JWTClaimsSet.Builder() //生成载荷payload
.jwtID(String.valueOf(new Random().nextLong())) //随机ID号
//令牌受众,使用URLEncoder.encode编码,避免中文乱码问题
.audience(URLEncoder.encode(username, StandardCharsets.UTF_8))
//令牌有效期设置为20分钟
.expirationTime(new Date(today.getTime() + 60000 * 20))
.notBeforeTime(today) //生效时间
.subject("tams") //主题
.issuer("tams.com") //令牌签发人
.build();
SignedJWT signedJWT = new SignedJWT(header, payload);
JWSSigner signer = new RSASSASigner(rsaJWK);
signedJWT.sign(signer); //签名
//创建JWE(JSON Web Encryption)对象以便进行加密处理
JWEObject jweObject = new JWEObject(
new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
.contentType("JWT")
.build(),
new Payload(signedJWT));
jweObject.encrypt(new RSAEncrypter(publicKey)); //使用公钥加密
//将jweObject对象序列化为Base64URL编码的字符串
jweString = jweObject.serialize();
} catch (Exception e) {
throw new RuntimeException(e);
}
return jweString;
}
public Boolean verifyToken(String token, String username) {
boolean isLegal = false;
try {
JWEObject jweObject = JWEObject.parse(token); //解析
jweObject.decrypt(new RSADecrypter(rsaJWK)); //解密
SignedJWT signedJWT = jweObject.getPayload().toSignedJWT();
JWSVerifier verifier = new RSASSAVerifier(publicKey);
if (signedJWT.verify(verifier)) { //校验令牌
JWTClaimsSet payload = signedJWT.getJWTClaimsSet();
boolean expires = new Date().before(payload.getExpirationTime());
String iss = payload.getIssuer();
String sub = payload.getSubject();
String aud = URLDecoder.decode(payload.getAudience().get(0),
StandardCharsets.UTF_8); //解码,可有效避免中文乱码
isLegal = expires && iss.equals("tams.com")
&& sub.equals("tams") && aud.equals(username); //是否有效
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return isLegal;
}
//对密码进行加密
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
下图显示用户登录成功后,通过浏览器的“检查”->“网络”页面,观察到的后端返回给前端HTTP消息头Authorization中存放的JWT令牌数据。
6.4 后端服务模块
6.4.1 模块的整体结构
先做准备工作:在app-server/user-service模块的src/main/java文件夹下创建好相应的包结构,如下图所示。
6.4.2 修改build.gradle构建脚本
修改app-server/user-service/build.gradle的构建脚本并重载更改:
plugins {
id 'server.common'
id 'java'
}
dependencies {
implementation project(':app-server:app-domain') //依赖实体类模块
compileOnly project(':app-server:app-util') //依赖工具类模块
}
6.4.3 配置application-users.yaml
修改app-server/user-service模块的resources/application-users.yaml,设置好用户登录的路由地址:
users:
route:
path: /usr,/login
显然,users.route.path的值有多个,是一个数组!
6.4.4 DTO类
DTO(Data Transfer Object)数据传输对象,常用于在视图层与服务层之间的数据传输。与实体类不同,DTO可根据页面数据传输的处理需要,来组织具体的属性:可以包含数据表中不存在的数据,也可以不包含数据表的某个字段。
由此看来,DTO更注重数据本身,不与实际业务发生联系,而是基于视图层数据传递的需要而设计,而实体类往往与数据表挂钩,也与业务逻辑紧密关联。
在app-server/user-service模块的com.tams.dto包下新建UsersDto.java类:
package com.tams.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UsersDto {
private String username;
private String password;
private String role;
private String email;
}
这个类跟前面的实体类Users略有差异:没有logo属性,因为前端登录页面,并不需要logo。
6.4.5 编写登录服务
(1)编写IUsersService接口类
在app-server/user-service模块的com.tams.repository包下新建IUsersRepository.java接口类:
package com.tams.repository;
import com.tams.entity.Users;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
public interface IUsersRepository extends R2dbcRepository<Users, String> {
}
我们后面只需要利用R2dbcRepository接口提供的方法,因此该接口类不需要写任何代码!
(2)创建业务逻辑处理类UsersHandler
在app-server/user-service模块的com.tams.handler包下新建UsersHandler.java类:
package com.tams.handler;
import com.tams.Assistant;
import com.tams.dto.UsersDto;
import com.tams.entity.Users;
import com.tams.repository.IUsersRepository;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
import static org.springframework.web.reactive.function.server.ServerResponse.status;
@Component
@RequiredArgsConstructor
public class UsersHandler {
private final @NonNull IUsersRepository usersRepository;
private final @NonNull Assistant assistant; //注入Assitant实例对象
private final @NonNull ModelMapper modelMapper; //注入ModelMapper实例对象
public Mono<ServerResponse> login(ServerRequest request) {
return request.bodyToMono(UsersDto.class) //请求数据提取为DTO类型Mono
.map(this::dtoUser) //DTO对象转换为实体对象
.flatMap(this::doLogin); //执行登录业务
}
//将DTO类映射为实体类
private Users dtoUser(UsersDto userDto) {
return modelMapper.map(userDto, Users.class);
}
private Mono<ServerResponse> doLogin(Users user) {
return usersRepository.findById(user.getUsername()) //根据用户名查询数据
.filter(dbUser -> assistant.passwordEncoder()
.matches(user.getPassword(), dbUser.getPassword())) //匹配密码
.flatMap(this::responseData) //登录成功,返回相应数据
.switchIfEmpty(status(NO_CONTENT).build()) //登录失败
.onErrorResume(e -> status(EXPECTATION_FAILED).build()); //出错
}
//构造返回给前端的数据
private Mono<ServerResponse> responseData(Users user) {
Map<String, String> map = new HashMap<>(3);
map.put("username", user.getUsername());
map.put("logo", user.getLogo());
map.put("role", user.getRole());
//通过HTTP响应头属性Authorization,向前端返回令牌,并返回Map数据
return ok().header("Authorization", assistant.getToken(user.getUsername()))
.bodyValue(map);
}
}
ModelMapper用于在实体类、DTO类之间进行对象转换。在教务辅助管理系统中,主要用于将DTO类对象转换成实体类对象。因此,需要事先定义好相应的Bean。考虑到其全局应用特点,我们将其定义在app-server/app-boot模块中。打开app-server/app-boot模块的AppBootConfig.java,加入ModelMapper:
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
6.4.6 定义函数式路由映射Bean
现在,需要将用户登录的路由路径与相应的处理程序进行映射。在app-server/user-service模块的com.tams.config包下新建UserServiceConfig.java类:
package com.tams.config;
import com.tams.handler.UsersHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class UserServiceConfig {
private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);
@Value("${users.route.path}") //application-users.yaml中配置的属性users.route.path
private String[] usrEndPoint;
@Bean
public RouterFunction<ServerResponse> userRoute(UsersHandler handler) {
return route()
.path(usrEndPoint[0], r -> r
.POST(usrEndPoint[1], ACCEPT_JSON, handler::login))
.build();
}
}
代码将路径端点usrEndPoint与后端服务handler对应起来。其中,usrEndPoint[0]的值为:/usr;usrEndPoint[1]的值为:/login。因此UsersHandler的login()方法的POST地址是:/usr/login,即前端用POST模式提交/usr/login时,将调用UsersHandler类的login()方法。
至此,用户登录的后端部分,全部完成!
下一步:用户登录(后端服务模块),且听下回分解。
标签:Vue,java,Spring,app,JWT,js,import,server,com From: https://blog.csdn.net/acoease/article/details/143422146