首页 > 其他分享 >响应式项目(RxJS+Vue.js+Spring)大决战(6):用户登录(后端服务模块)

响应式项目(RxJS+Vue.js+Spring)大决战(6):用户登录(后端服务模块)

时间:2024-11-01 09:47:59浏览次数:5  
标签:Vue java Spring app JWT js import server com

书接上篇:响应式项目(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

用户角色

email

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

相关文章