业务具体实现

分析

开始前把逻辑理清楚

所有的页面大概分为两大类——公开页面(login等)和授权页面(/)

授权是怎么实现的 前面我们使用的是sessionid

用户登入时 会查询数据库中该用户的信息 将用户输入的用户名和密码与数据库存储的用户名和密码进行比对(有加密) 如果一致 会给用户发送一个sessionid(类似于身份证号) 用户收到id会把它存在本地(cookie)之后每次在访问授权页面时 都会将存储的这个id发给服务器 服务端也会存下它(或是缓存或是数据库) 服务器会判断这个seesion是否有效 若有效 则从服务器端对应的controller信息提取填入上下文当中

现在换一种验证方式 传统的session的模式不是很方便 如果涉及到跨域就很蛋疼 现在有种流行的jwt验证方式 它可以很容易的实现跨域 也不需要在服务器端存储 以一个令牌可以登入多个服务器

原理也差不多 一个服务器登入后 不再是给session 而是给一个token 服务器给了用户一个token后 自己是不用存任何东西的

那怎么验证身份呢 它将userid或者一些用户信息 加在密钥里面 大概可以看成两部分组成 用户信息+服务器也没存储的密钥

然后通过一些特定的加密算法 把这个字符串映射成新字符串

image-f9089aad

之后 将userid那一段和新生成的那段 拼接起来 返回给用户 作为jwt-token

image-a5049137

未来服务器验证也很简单 把userid加上密钥 再用这种加密算法算一遍 如果结果一样 即验证成功

具体来看:JWT验证

那么抽象一下 首先通过登入页面 获取一个token 用户得到token后 存在浏览器里面 每次发送请求时 都会带上token 服务器端接收到请求 发现如果是受限链接 就会验证这个token 如果合法 就根据这个token 里面的用户信息 从数据库查找出对应user的内容 提取到上下文之中 然后才可以访问需要授权的页面

这里提供几个工具类

utils.JwtUtil类 config.filter.JwtAuthenticationTokenFilter类 config.SecurityConfig类

引入依赖

  • jjwt-api
  • jjwt-impl
  • jjwt-jackson

Maven仓库地址

现在不敢乱加新版本了 用chatgpt推荐的对应springboot3.1.1的老版本

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

工具类

utils.JwtUtil

功能

  1、将一个字符串 加上密钥 加上有效期 把他变成一个加密后的字符串

  2、得到一个token 从中解析出userid

在backend包下建utils包(不知道放哪的文件就放在这个工具包下)

新建JwtUtil类 把前面的给的代码复制进去

image-5ee07a2e
 package com.zwnsyw.backend.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

@Component
public class JwtUtil {
    public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14;  // 有效期14天
    public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";

    public static String getUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, 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)
                .setSubject(subject)
                .setIssuer("sg")
                .setIssuedAt(now)
                .signWith(signatureAlgorithm, secretKey)
                .setExpiration(expDate);
    }

    public static SecretKey generalKey() {
        byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
    }

    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(jwt)
                .getBody();
    }
}

config.filter.JwtAuthenticationTokenFilter

功能:

 验证token是否合法 合法则将user提取到上下文中

需要补入一个依赖

解决

import org.jetbrains.annotations.NotNull;

爆红问题

	<dependency>
			<groupId>org.jetbrains</groupId>
			<artifactId>annotations</artifactId>
			<version>24.0.0</version> <!-- 版本号可以根据需要更改 -->
		</dependency>

这俩不清楚要不要加 后面调了蛮久发现没用到

<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>4.0.1</version> <!-- 版本号可根据需要选择 -->
		</dependency>

		<dependency>
			<groupId>jakarta.servlet</groupId>
			<artifactId>jakarta.servlet-api</artifactId>
			<version>5.0.0</version>
			<scope>provided</scope>
		</dependency>
image-663fd860

改后得到的代码

package com.zwnsyw.backend.config.filter;

import com.zwnsyw.backend.mapper.UserMapper;
import com.zwnsyw.backend.pojo.User;
import com.zwnsyw.backend.service.impl.utils.UserDetailsImpl;
import com.zwnsyw.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;

import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private UserMapper userMapper;

    @Override
    protected void doFilterInternal(@NotNull jakarta.servlet.http.HttpServletRequest request,
                                    @NotNull jakarta.servlet.http.HttpServletResponse response,
                                    @NotNull jakarta.servlet.FilterChain filterChain)
            throws jakarta.servlet.ServletException, IOException {
        String token = request.getHeader("Authorization");

        if (!org.springframework.util.StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        token = token.substring(7);

        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        User user = userMapper.selectById(Integer.parseInt(userid));

        if (user == null) {
            throw new RuntimeException("用户名未登录");
        }

        UserDetailsImpl loginUser = new UserDetailsImpl(user);
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, null);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request, response);
    }
}

config.SecurityConfig

前面对SecurityConfig做了些配置 现在需要完善它

image-b895d172
package com.zwnsyw.backend.config;

import com.zwnsyw.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
    }

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

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable()) // 直接通过lambda表达式禁用CSRF保护
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 设置为无状态
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/user/account/token/", "/user/account/register/").permitAll()
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
                        .anyRequest().authenticated()
                );

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

@Configuration @EnableWebSecurity 注解

  • @Configuration 表示这是一个配置类,Spring 会将其视为一个配置 Bean。
  • @EnableWebSecurity 启用 Spring Security 的 Web 安全功能,这是配置安全过滤器链和安全策略的基础。

注入 JwtAuthenticationTokenFilter

private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
    this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
}

这部分代码通过构造函数注入 JwtAuthenticationTokenFilter,这是自定义的 JWT 身份验证过滤器,用于处理 JWT 令牌的解析和验证。

PasswordEncoder Bean

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

配置一个 PasswordEncoder Bean,采用 BCryptPasswordEncoder 来对密码进行加密。这个加密器可以确保用户密码存储的安全性。

AuthenticationManager Bean

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
}

定义一个 AuthenticationManager,这个 Bean 用于处理用户认证操作。AuthenticationConfiguration 提供了默认的认证管理器配置。

SecurityFilterChain 配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable()) // 禁用 CSRF(跨站请求伪造)保护
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 设置为无状态
        .authorizeHttpRequests(authorize -> authorize
            .requestMatchers("/user/account/token/", "/user/account/register/").permitAll() // 允许这些端点无需身份验证
            .requestMatchers(HttpMethod.OPTIONS).permitAll() // 允许 OPTIONS 请求(一般用于跨域请求的预检请求)
            .anyRequest().authenticated() // 其他所有请求都需要身份验证
        );

    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 在用户名密码过滤器之前添加自定义的 JWT 过滤器

    return http.build();
}
  • 禁用 CSRF 保护:由于该应用是无状态的(不使用会话),所以禁用 CSRF 保护,这在 RESTful API 中是常见做法。
  • 无状态的会话管理:通过 SessionCreationPolicy.STATELESS 配置,表示 Spring Security 不会创建或使用 HTTP Session,这使得应用无状态。
  • 请求授权规则:允许访问 /user/account/token//user/account/register/ 端点,而其他所有请求都需要进行身份验证。
  • 添加自定义 JWT 过滤器:将 JwtAuthenticationTokenFilter 添加到过滤器链中,且放置在用户名密码过滤器之前。这个自定义的过滤器会负责从请求中提取 JWT 令牌并进行验证。

功能总结:

  • 该配置类用于定义和管理应用程序的安全策略。它通过禁用 CSRF 保护、设置无状态会话、定义允许的请求路径、以及使用自定义的 JWT 过滤器来实现基于 JWT 的身份验证机制。
  • 密码使用 BCryptPasswordEncoder 进行加密存储,确保用户的密码安全。
  • 在安全过滤器链中,通过 authorizeHttpRequests 来配置不同端点的访问权限。

主要要动的地方就是

image-31cf6b9c

这部分添加的链接 表示为公开的 无需验证

之后需要加哪个页面 允许公开访问 就只需要在这加入链接 用逗号隔开

创建后端api

准备工作

修改表结构

修改id为主键、唯一、非null、自增

image-a5855085

新增列photo 存储用户头像 因为存的是链接可能比较长 设置为varchar(1000)

image-eac235eb

得到:

image-bc004061

暂时用acwing的头像 https://cdn.acwing.com/media/user/profile/photo/257728_lg_bb7e075a72.jpg

之后这些东西可以搭一个图床

配置Pojo

backend/pojo/User.java

package com.zwnsyw.backend.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @TableId(type= IdType.AUTO)
    private Integer id;
    private String username;
    private String password;
    private String photo;
}

新增photo字段 另外用@Tableld注解实现id的自增

写具体业务

三个api

实现/user/account/token/:验证用户名密码,验证成功后返回jwt token(令牌)

实现/user/account/info/:根据令牌返回用户信息

实现/user/account/register/:注册账号

写api有个流程

1、先写controller 用于调用service接口

2、在service 写接口

3、在service中的impl里写具体实现

约定俗成

service 接口

创建目录结构

image-92ebdbf6

也一般是以板块来分 这是user板块的接口 所以在service包下建一个user包 然后该包下可能有很多板块 账号啊 机器人啊什么的 这三个属于账号相关的 再给它封装在account包下

接口的数据一般习惯使用map返回

InfoService:

package com.zwnsyw.backend.service.user.account;

import java.util.Map;

public interface InfoService {
    public Map<String,String> getInfo();
}

LoginService:

package com.zwnsyw.backend.service.user.account;

import java.util.Map;

public interface LoginService {
     public Map<String,String> getToken(String username, String password);
}

RegisterService:

package com.zwnsyw.backend.service.user.account;

import java.util.Map;

public interface RegisterService {
    public Map<String, String> register(String username, String password, String confirmedPassword);
}

service/impl 具体实现

LoginServiceImpl
image-5bc25db7

加上@Service注解说明是Service

implements LoginService 说明实现该接口

alt+insert 实现方法

得到

package com.zwnsyw.backend.service.impl.user.account;

import com.zwnsyw.backend.service.user.account.LoginService;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class LoginServiceImpl implements LoginService {
    @Override
    public Map<String, String> getToken(String username, String password) {
        return Map.of();
    }
}

登入的话需要先验证用户是否登入成功

需要用到AuthenticationManager 把它注入进来

@Autowired
    private AuthenticationManager authenticationManager;
  • 接收用户名和密码作为参数,返回一个包含错误信息和JWT令牌的Map
  • 方法内部首先使用用户名和密码创建了一个UsernamePasswordAuthenticationToken实例,这是Spring Security用于封装认证信息的类。
  • 然后,调用authenticationManager.authenticate(authenticationToken)进行认证。如果认证失败,这个方法会抛出异常(通常被Spring Security的异常处理机制捕获并处理)。
  • 认证成功后,从返回的Authentication对象中获取Principal(在这里是UserDetailsImpl类型),进而获取到User实体。
  • 使用JwtUtil.createJWT(user.getId().toString())生成JWT令牌。这里假设JWT令牌是基于用户ID生成的。
  • 最后,创建一个Map,设置错误消息为"success",并将JWT令牌放入Map中返回。
package com.zwnsyw.backend.service.impl.user.account;

import com.zwnsyw.backend.pojo.User;
import com.zwnsyw.backend.service.impl.utils.UserDetailsImpl;
import com.zwnsyw.backend.service.user.account.LoginService;
import com.zwnsyw.backend.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

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

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public Map<String, String> login(String username, String password) {

        //把密文的用户名和密码封装成能用的密文形式
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username, password);

        //需要传入一个用户名和密码 需要先封装出来 因为数据库里存的是明文(加密后的乱码) 不能直接使用
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);//快捷键.var 自动封装出来 //如果登入失败会自动处理 里面有异常处理
        UserDetailsImpl loginUser=(UserDetailsImpl) authenticate.getPrincipal();
        User user=loginUser.getUser();

        String jwt= JwtUtil.createJWT(user.getId().toString());

        Map<String,String> map=new HashMap<>();
        map.put("error_message","success");
        map.put("token",jwt);

        return map;
    }
}
InfoServiceImpl
image-4824ced4

老规矩 加@Service 、实现自InfoService 、alt + insert 实现方法

package com.zwnsyw.backend.service.impl.user.account;

import com.zwnsyw.backend.pojo.User;
import com.zwnsyw.backend.service.impl.utils.UserDetailsImpl;
import com.zwnsyw.backend.service.user.account.InfoService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

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

@Service
public class InfoServiceImpl implements InfoService {
    @Override
    public Map<String, String> getInfo() {
        UsernamePasswordAuthenticationToken authentication =
                (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        UserDetailsImpl loginUser = (UserDetailsImpl) authentication.getPrincipal();
        User user = loginUser.getUser();

        Map<String,String> map = new HashMap<>();
        map.put("error_message","success");
        map.put("id",user.getId().toString());
        map.put("username",user.getUsername());
        map.put("photo",user.getPhoto());

        return map;
    }
}
  • 使用SecurityContextHolder.getContext().getAuthentication()获取当前的安全上下文中的认证信息。SecurityContextHolder是Spring Security中用于存储安全上下文信息的工具类。
  • 将认证信息强制类型转换为UsernamePasswordAuthenticationToken,这是Spring Security中用于表示用户名和密码认证信息的类。
  • UsernamePasswordAuthenticationToken中获取principal,即当前登录的用户信息,这里将其强制类型转换为UserDetailsImplUserDetailsImpl是自定义的类,实现了UserDetails接口,包含了用户的具体信息。
  • 通过UserDetailsImplgetUser方法获取到User对象,这是用户信息的实体类。
  • 创建一个HashMap来存储用户信息,包括一个固定的error_message字段(这里设置为"success"表示操作成功),以及用户的ID、用户名和照片。
  • 最后,返回这个包含用户信息的Map
RegisterServiceImpl

写一系列判断逻辑 验证注册时的账号密码是否合法

其中 是否重名需要用到查询数据库 注入userMapper

存储时加密密码 需要注入PasswordEncoder

package com.zwnsyw.backend.service.impl.user.account;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zwnsyw.backend.mapper.UserMapper;
import com.zwnsyw.backend.pojo.User;
import com.zwnsyw.backend.service.user.account.RegisterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class RegisrterServiceImpl implements RegisterService {
    //需要查询用户名不重复 注入Mapper中实现的增删改查
    @Autowired
    private UserMapper userMapper;

    //验证没问题后 添加到数据库中去 存的时候需要把密码加密 把该功能注入
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Map<String, String> register(String username, String password, String confirmedPassword) {

        Map<String,String> map = new HashMap<>();

        if(username==null){
            map.put("error_message","用户名不能为空");
            return map;
        }
        if(password == null || confirmedPassword == null){
            map.put("error_message","密码不能为空");
            return map;
        }

        username = username.trim();//删除用户名中的空格
        if(username.length()==0){
            map.put("error_message","用户名不能为空");
            return map;
        }

        if(username.length()>100){
            map.put("error_message","用户名长度不能大于100");
            return map;
        }

        if(password.length()==0 || confirmedPassword.length()==0){
            map.put("error_message","密码不能为空");
            return map;
        }

        if(password.length()>100 || confirmedPassword.length()>100){
            map.put("error_message","密码长度不能大于100");
            return map;
        }

        if(!password.equals(confirmedPassword)){
            map.put("error_message","两次输入的密码不一致");
            return map;
        }

        QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
        queryWrapper.eq("username",username);
        List<User> users = userMapper.selectList(queryWrapper);
        if(!users.isEmpty()){
            map.put("error_message","用户名已存在");
            return map;
        }

        //一系列验证完成 加入数据库中去
        String encodedPassword=passwordEncoder.encode(password);
        String photo = "https://cdn.acwing.com/media/user/profile/photo/257728_lg_bb7e075a72.jpg";//先搞个默认头像
        User user = new User(null,username,encodedPassword,photo);//id自增 无需传入
        userMapper.insert(user);

        map.put("error_message","success");
        return map;
    }
}

Controller调用接口

可以把之前调试增删改查用的Usercontroller删掉

user下定义一个account包 用于放三个调用接口

LoginController
image-9ea6f7bc

标明为@RestController

再将刚刚定义的接口注入进来

image-fe5df582

登入应该是一个post请求 (用户名密码) get请求是url链接 明文传输 不安全 而post看不到明文

image-6070b454

注意设置的入口需要在SecurityConfig中放行出来

image-08d5bc54

使用@RequestParam把请求参数绑定下来 得到代码:

package com.zwnsyw.backend.controller.user.account;

import com.zwnsyw.backend.service.user.account.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class LoginController {
    @Autowired
    private LoginService loginService;

//    @PostMapping("/user/account/token/")
//    public Map<String,String> getToken(@RequestParam Map<String,String> map){
//        String username=map.get("username");
//        String password=map.get("password");
//        return loginService.getToken(username,password);
//    }
    @PostMapping("/user/account/token/")
    public Map<String,String> getToken(@RequestParam String username, @RequestParam String password){
        return loginService.getToken(username,password);
    }
}
插曲 (提醒) 工具postman

打开调试发现 403

image-f7b9c0c8

这是因为 我们出于安全 写的是一个post请求 而浏览器访问给的是一个get请求

所以不能直接在浏览器里调

现在有两种选择

一个是使用工具 postman

另一个是直接用前端连上调

这里没讲第一种 但感觉还是要学这个工具的 以后总不可能全是全栈的项目吧

使用ajax调试

import $ from "jquery"

export default{
 setup(){
      $.ajax({
        url:"http://localhost:3000/user/account/token/",
        type:"post",
        data:{
          username:'zw',
          password:"123456",
        },
        success(resp){
          console.log(resp);
        },
        error(resp){
          console.log(resp);
        }
      })
    }
}

成功

image-dc6d8190
InfoController
image-331a00a2

标明为@RestController

注入InfoService

这里是得到一个信息 使用GetMapping

package com.zwnsyw.backend.controller.user.account;

import com.zwnsyw.backend.service.user.account.InfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class InfoController {

    @Autowired
    private InfoService infoService;

    @GetMapping("/user/account/info/")
    public Map<String,String> getinfo(){
        return infoService.getInfo();
    }

}

在index.vue中 使用ajax发送一个get请求

get请求的入口在backend/config/filter/JwtAuthenticationTokenFilter.java中

一般为Authorization

image-d6f2d1bd

把先前得到的token 使用Bearer+token的形式进行认证

注意 Bearer后一定要跟个空格

也是因为JwtAuthenticationTokenFilter中的配置

image-acd6d3be

App.vue中如下:

 $.ajax({
        url:"http://localhost:3000/user/account/info/",
        type:"get",
        headers:{
          Authorization:"Bearer " + "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1ZTg0NGFkZWRhZWY0YjBiYWIxYTIyYjc3ZjFmNzhjNSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTczMDI5NjE4MSwiZXhwIjoxNzMxNTA1NzgxfQ.iJEEK17abDkGO3NmkDPIxC6pmY6EZAJox2vg4PAXo7s"
        },
        success(resp){
          console.log(resp);
        },
        error(resp){
          console.log(resp);
        }
      })

成功得到用户信息

image-02cefd01
RegisterController
image-bc41f5ca

注册也是个post请求 因为密码不能明文传

从前端获取三个参数传给接口

记得在SecurityConfig中放行

image-575662c6
package com.zwnsyw.backend.controller.user.account;

import com.zwnsyw.backend.service.user.account.RegisterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class RegisterController {
    @Autowired
    private RegisterService registerService;

    @PostMapping("/user/account/register/")
    public Map<String,String> register(@RequestParam Map<String ,String> map){
        String username = map.get("username");
        String password = map.get("password");
        String confirmedPassword = map.get("confirmedPassword");
        return registerService.register(username,password,confirmedPassword);
    }

}

在前端进行调试

$.ajax({
        url:"http://localhost:3000/user/account/register/",
        type:"post",
        data:{
          username:"zw",
          password:"123456",
          confirmedPassword:"123456",
        },
        success(resp){
          console.log(resp);
        },
        error(resp){
          console.log(resp);
        }
      })
image-6f38a913

至此登入注册模块的后端部分基本写完

思考拓展

前面的内容频繁接触到了一些东西

比如get post 请求 有关密码的请求都得用post请求 因为它不和get一样用url式的明文传输 更加安全

调试上 用到了ajax的一些方法 其实之前或多或少也见过 但没特别去学这个用法

还有一种不在前端调试的方法 使用postman工具 现在也拓展一下

以及@Autowired注解 用起来感觉像是 导包 就是要用到哪部分功能 就给他注入当前工作域

然后@Service @RestController @Mapper 等注解就是说明是哪一部分内容 方便springboot找到并管理

@RequestParam注解就是把网页传来的东西绑定在java的某个变量上 用Map存的话 就不用写那么长的参数

image-df5e8163

你的理解是正确的。你提到的内容中,关于GET和POST请求的差异、安全性,以及在需要传输敏感数据时使用POST请求,这是Web开发中的一个关键概念。GET请求的参数是通过URL明文传输的,而POST请求则是将参数包含在请求体中,因此在处理敏感信息时更加安全。

对于AJAX的使用和Postman工具的调试,你已经掌握了基本的调试方法。AJAX用于在不刷新页面的情况下与服务器通信,而Postman则可以帮助你快速调试和测试API请求。

@Autowired注解确实是一种依赖注入(Dependency Injection)的方式,可以自动将需要的组件注入到当前类中,简化了对象的创建和管理。而@Service、@RestController、@Mapper等注解的确起到了标识作用,它们告诉Spring Boot这些类的具体用途,让框架能更好地进行管理。

至于@RequestParam注解,你也理解得很到位。它能够将HTTP请求的参数自动绑定到方法参数中,使用Map时可以简化代码。

Http请求 以及 Springboot注解

前端请求工具(HTTP请求库)

Postman

前端代码编写

image-4114c2cd

记得在router中加入这俩页面

 {
    path:"/user/account/login/",
    name:"user_account_login",
    component:UserAccountLoginView,
  },
  {
    path:"/user/account/register/",
    name:"user_account_register",
    component:UserAccountRegisterView,
  },
image-83680629 image-3a6c27a6

登录页面

大概样式实现

bootstrip中找

<template>
  <ContentField>
    <div class="row justify-content-md-center">
      <div class="col-3">
        
          <div class="mb-3">
            <label for="username" class="form-label">用户名</label>
            <input type="text" class="form-control" id="username" placeholder="请输入姓名">
          </div>
          <div class="mb-3">
            <label for="password" class="form-label">密码</label>
            <input type="password" class="form-control" id="password" placeholder="请输入密码">
          </div>
          <div class="error-message">密码错误</div>
          <button class="btn btn-primary" type="submit">提交</button>
        
      </div>
    </div>
  </ContentField>
</template>


import ContentField from '@/components/ContentField.vue'

export default{
  components:{
    ContentField
  }
}



button {
  width: 100%;
}
div.error-message{
  color: #ff416c;
}

image-ae31f34a

全局信息和登入请求

另外 我们需要存储一些全局信息 在每个页面都需要存一下当前登入的用户是谁 所以需要把用户信息存在全局的

这里需要用到vuex

用户相关的信息 存在一个新的模块里面

image-d1f2bcef

vue存全局信息一般都是存store/index.js里面的这几个东西

image-a0029aeb

直接给他复制到User,js里面去 定义好要用的变量和set接口

export default {
    state: {
        id: "",
        username: "",
        photo: "",
        token: "",
        is_login: false,
    },
    getters: {
    },
    mutations: {//用于修改数据
        updateUser(state,user){
            state.id=user.id;
            state.username=user.username;
            state.photo=user.photo;
            state.is_login=user.is_login;
        },
        updateToken(state,token){
            state.token=token;
        }
    },
    actions: {
    },
    modules: {
    }
}

在全局配置的store/index.js中引入

store/index.js:

import { createStore } from 'vuex'
import ModuleUser from './User'

export default createStore({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    user:ModuleUser,
  }
})

接下来实现登入

其实就是调试时的post请求 现在的用户名密码是传入的而非手写的 如果成功请求 就把得到的token写入全局

store/User.js

import $ from "jquery";

export default {
    state: {//类似定义变量
        id: "",
        username: "",
        photo: "",
        token: "",
        is_login: false,
    },
    getters: {
    },
    mutations: {//用于修改数据
        updateUser(state,user){
            state.id=user.id;
            state.username=user.username;
            state.photo=user.photo;
            state.is_login=user.is_login;
        },
        updateToken(state,token){
            state.token=token;
        }
    },
    actions: {
        login(context,data){
            $.ajax({
                url:"http://localhost:3000/user/account/token/",
                type:"post",
                data:{
                    username: data.username,
                    password: data.password,
                },
                success(resp){ //如果成功 需要将token存下来
                    if(resp.error_message === "success"){
                        context.commit("updateToken", resp.token);    //在actions中调用mutations中的函数 需要用commit+字符串
                        data.success(resp);
                    }else{
                        data.error(resp);
                    }
                },
                error(resp){
                    data.error(resp);
                }
            });
        }
    },
    modules: {
    }
}

LoginView中应用登入逻辑

<template>
  <ContentField>
    <div class="row justify-content-md-center">
      <div class="col-3">
        
          <div class="mb-3">
            <label for="username" class="form-label">用户名</label>
            <input v-model="username" type="text" class="form-control" id="username" placeholder="请输入姓名">
          </div>
          <div class="mb-3">
            <label for="password" class="form-label">密码</label>
            <input v-model="password" type="password" class="form-control" id="password" placeholder="请输入密码">
          </div>
          <div class="error-message">{{error_message}}</div>
          <button class="btn btn-primary" type="submit">提交</button>
        
      </div>
    </div>
  </ContentField>
</template>


import ContentField from '@/components/ContentField.vue'
import {useStore} from "vuex";
import {ref}  from 'vue'

export default{
  components:{
    ContentField
  },
  setup(){
    const store = useStore();
    let username = ref('');
    let password = ref('');
    let error_message = ref('');

    const login = () => {
        error_message.value = "";
        store.dispatch("login",{
          username:username.value,
          password:password.value,
          success(resp){
            console.log(resp);
          },
          error(){
            error_message.value = "用户名或密码错误";
          }
        })
    }

    return {
      username,
      password,
      error_message,
      login,
    }
  }
}



button {
  width: 100%;
}
div.error-message{
  color: #ff416c;
}

image-98c47fe8 image-46d93ce0

如果验证成功 就跳转到主页面

import router from "@/router/index";

 const login = () => {
        error_message.value = "";
        store.dispatch("login",{
          username:username.value,
          password:password.value,
          success(){
            router.push({name:"home"});
          },
          error(){
            error_message.value = "用户名或密码错误";
          }
        })
    }
PixPin_2024-10-31_02-46-52-a3971aa5

获取用户信息

登入成功后 需要动态的显示出我们的信息 如昵称 头像等等

所以在登录完后 还要向后端发送请求 获取用户信息

在store/user.js中再写一个辅助函数 其实就是调试infoController时做的get请求

User,js

import $ from "jquery";

export default {
    state: {//类似定义变量
        id: "",
        username: "",
        photo: "",
        token: "",
        is_login: false,
    },
    getters: {
    },
    mutations: {//用于修改数据
        updateUser(state,user){
            state.id=user.id;
            state.username=user.username;
            state.photo=user.photo;
            state.is_login=user.is_login;
        },
        updateToken(state,token){
            state.token=token;
        }
    },
    actions: {
        login(context,data){
            $.ajax({
                url:"http://localhost:3000/user/account/token/",
                type:"post",
                data:{
                    username: data.username,
                    password: data.password,
                },
                success(resp){ //如果成功 需要将token存下来
                    if(resp.error_message === "success"){
                        context.commit("updateToken", resp.token);    //在actions中调用mutations中的函数 需要用commit+字符串
                        data.success(resp);
                    }else{
                        data.error(resp);
                    }
                },
                error(resp){
                    data.error(resp);
                }
            });
        },
        getinfo(context,data){
            $.ajax({
                url:"http://localhost:3000/user/account/info/",
                type:"get",
                headers:{
                    Authorization:"Bearer " + context.state.token,
                },
                success(resp){
                    if(resp.error_message === "success"){
                        context.commit("updateUser",{
                            ...resp,//将resp解构出来
                            is_login: true,
                        });
                        data.success(resp);
                    }else{
                        data.error(resp);
                    }
                },
                error(resp){
                    data.error(resp);
                }
            });
        }
    },
    modules: {
    }
}

在LoginView中调用

const login = () => {
        error_message.value = "";
        store.dispatch("login",{
          username:username.value,
          password:password.value,
          success(){
            store.dispatch("getinfo",{
              success(){
                router.push({ name : 'home' });
                console.log(store.state.user);
              }
            })
          },
          error(){
            error_message.value = "用户名或密码错误";
          }
        })
    }
image-4fdd1743

成功

应用到导航栏

获取到这些信息后 给展示到页面上

用v-if v-else 进行渲染 如果登入成功 就展示个人信息 否则展示登录注册按钮

 <ul class="navbar-nav" v-if="$store.state.user.is_login">
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                            {{$store.state.user.username}}
                        </a>
                        <ul class="dropdown-menu">
                            <li>
                                <!-- <a class="dropdown-item" href="#root/tljkJjy1OBR0">我的Bot</a> -->
                                <router-link class="dropdown-item" :to="{name: 'user_bot_index'}">我的Bot</router-link>
                            </li>
                            <li><hr class="dropdown-divider"></li>
                            <li><a class="dropdown-item" href="#">退出</a></li>
                        </ul>
                    </li>
                </ul>

                <ul class="navbar-nav" v-else>
                  <li class="nav-item">
                    <router-link class="nav-link" :to="{name:'user_account_login'}" role="button">
                      登录
                    </router-link>
                  </li>
                  <li class="nav-item">
                    <router-link class="nav-link" :to="{name:'user_account_register'}" role="button">
                      注册
                    </router-link>
                  </li>
                </ul>

退出登录

前面的认证机制可以发现 token完全存在用户本地 要让认证失败 直接本地把token删了即可 所以这一步不需要后端的介入

(其实现在有个bug 因为token存在浏览器内存里面 只要一刷新 登入就失效了 后续会存在localstorage里面 即使关闭再打开也不会失效)

在user,js的mutations中写一个退出的修改状态函数

 mutations: {//用于修改数据
        updateUser(state,user){
            state.id=user.id;
            state.username=user.username;
            state.photo=user.photo;
            state.is_login=user.is_login;
        },
        updateToken(state,token){
            state.token=token;
        },
        logout(state){
            state.id="";
            state.username="";
            state.photo="";
            state.token="";
            state.is_login=false;
        }
    },

再在actions中写个接口

 logout(context){
            context.commit("logout");
        }

User.js:

import $ from "jquery";

export default {
    state: {//类似定义变量
        id: "",
        username: "",
        photo: "",
        token: "",
        is_login: false,
    },
    getters: {
    },
    mutations: {//用于修改数据
        updateUser(state,user){
            state.id=user.id;
            state.username=user.username;
            state.photo=user.photo;
            state.is_login=user.is_login;
        },
        updateToken(state,token){
            state.token=token;
        },
        logout(state){
            state.id="";
            state.username="";
            state.photo="";
            state.token="";
            state.is_login=false;
        }
    },
    actions: {
        login(context,data){
            $.ajax({
                url:"http://localhost:3000/user/account/token/",
                type:"post",
                data:{
                    username: data.username,
                    password: data.password,
                },
                success(resp){ //如果成功 需要将token存下来
                    if(resp.error_message === "success"){
                        context.commit("updateToken", resp.token);    //在actions中调用mutations中的函数 需要用commit+字符串
                        data.success(resp);
                    }else{
                        data.error(resp);
                    }
                },
                error(resp){
                    data.error(resp);
                }
            });
        },
        getinfo(context,data){
            $.ajax({
                url:"http://localhost:3000/user/account/info/",
                type:"get",
                headers:{
                    Authorization:"Bearer " + context.state.token,
                },
                success(resp){
                    if(resp.error_message === "success"){
                        context.commit("updateUser",{
                            ...resp,//将resp解构出来
                            is_login: true,
                        });
                        data.success(resp);
                    }else{
                        data.error(resp);
                    }
                },
                error(resp){
                    data.error(resp);
                }
            });
        },
        logout(context){
            context.commit("logout");
        }
    },
    modules: {
    }
}

在导航栏NavBar.vue里加一个函数

 import { useStore} from "vuex";

 export default{
        setup(){

            const store = useStore();

            const logout = () => {
              store.dispatch("logout");
            }

            return{
                logout
            }
        }
    }

然后给退出按钮绑定事件

<li><a class="dropdown-item" href="#" @click="logout">退出</a></li>

完整NavBar.vue如下

<template>
    <nav class="navbar navbar-expand-lg  bg-body-tertiary">
        <div class="container">
            <!-- <a class="navbar-brand" href="#root/3SVYnqcfVbzy">King Of Bots</a> -->

            <router-link class="navbar-brand" :to="{name: 'home'}">King Of Bots</router-link>

            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="navbarText">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                    <!-- <a class="nav-link active" aria-current="page" href="#root/ahtbHm373fs1">对战</a> -->
                    <router-link :class="route-name=='pk_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'pk_index'}">对战</router-link>
                    </li>
                    <li class="nav-item">
                    <!-- <a class="nav-link" href="#root/NJAzxBXZsU9R">对局列表</a> -->
                    <router-link :class="route-name=='record_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'record_index'}">对局列表</router-link>
                    </li>
                    <li class="nav-item">
                    <!-- <a class="nav-link" href="#root/o64yaAapg7qV">排行榜</a> -->
                    <router-link :class="route-name=='ranklist_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'ranklist_index'}">排行榜</router-link>
                    </li>
                </ul>

                <ul class="navbar-nav" v-if="$store.state.user.is_login">
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                            {{$store.state.user.username}}
                        </a>
                        <ul class="dropdown-menu">
                            <li>
                                <!-- <a class="dropdown-item" href="#root/tljkJjy1OBR0">我的Bot</a> -->
                                <router-link class="dropdown-item" :to="{name: 'user_bot_index'}">我的Bot</router-link>
                            </li>
                            <li><hr class="dropdown-divider"></li>
                            <li><a class="dropdown-item" href="#" @click="logout">退出</a></li>
                        </ul>
                    </li>
                </ul>

                <ul class="navbar-nav" v-else>
                  <li class="nav-item">
                    <router-link class="nav-link" :to="{name:'user_account_login'}" role="button">
                      登录
                    </router-link>
                  </li>
                  <li class="nav-item">
                    <router-link class="nav-link" :to="{name:'user_account_register'}" role="button">
                      注册
                    </router-link>
                  </li>
                </ul>

            </div>
        </div>
    </nav>
</template>


    import { useRoute } from 'vue-router';
    import { computed } from 'vue';
    import { useStore} from "vuex";

    export default{
        setup(){
            const route=useRoute();
            const store = useStore();

            let route_name=computed(() =>route.name)

            const logout = () => {
              store.dispatch("logout");
            }

            return{
                route_name,
                logout
            }
        }
    }





PixPin_2024-10-31_03-53-29-df31ff54

切换页面不掉登录状态 用户登录右上角显示自己的账号 退出登入 已经实现

发现还有很多问题需要完善

比如登入如果账号不存在应该提示不存在该账号而不是用户名或密码错误

未登录状态还可以访问其他页面 这也是不合理的 应该加上权限控制

注册逻辑

我的修改

我给修改成了注册登入在同一个模块

如果没授权时 默认重定向到这个注册登入页面 它虽然只有一个页面但可以切换成登入或是注册 然后授权成功后 可以访问网站里的资源 并把自己的用户信息填充在导航栏的右上角 比如头像昵称等等 然后有个退出登入按钮 退出后直接又到了这个注册登入页面

PixPin_2024-10-31_05-01-04-a23c85c5

所以后面可能不是跟着y总做 但其实变化也不大 前端只不过是样式 样式写在一个文件里和写在两个文件里 没什么大区别

注意关注y总的注册逻辑实现就好了

可以看到这个页面其实是没有导航栏的 但是导航栏组件是用在了App.vue 按道理全部页面都会有 这个是怎么删掉的

其实也是路由配置 和 类似vue-if的渲染问题

在路由配置里加上一句 meta: { hideNavBar: true }

  {
    path:"/user/account/login/",
    name:"user_account_login",
    component:UserAccountLoginView,
    meta: { hideNavBar: true }
  },

App.vue中

改成

即可

另外 在vue项目中 要引入什么图标库 或者其他什么东西 都得用npm

比如这里引入一些图标

npm install @fortawesome/fontawesome-free

然后在App.vue中引入

import '@fortawesome/fontawesome-free/css/all.css';
import '@fortawesome/fontawesome-free/js/all.js';

实现前端页面的授权

就是有些页面只能登入后才能看 而有些不登录就能看 如果没登录而进入了需要授权的页面 就会重定向回某个设定的页面

这可以通过路由守卫来完成

router.beforeEach((to, from, next)

to:跳转到哪个页面

from:从哪个页面跳转而来

next:页面要不要执行下一步操作

在这之前就需要标记每一个页面是不是授权页面

const routes = [
  {
    path:"/",
    name:"home",
    redirect:"/pk/",
    meta:{
      requestAuth:true,
    }
  },
  {
    path:"/pk/",//把http://localhost:8080/pk/
    name:"pk_index",
    component:PkIndexView,//映射到PkIndexView组件
    meta:{
      requestAuth:true,
    }
  },
  {
    path:"/record/",
    name:"record_index",
    component:RecordIndexView,
    meta:{
      requestAuth:true,
    }
  },
  {
    path:"/ranklist/",
    name:"ranklist_index",
    component:RanklistIndexView,
    meta:{
      requestAuth:true,
    }
  },
  {
    path:"/user/bot/",
    name:"user_bot_index",
    component:UserBotIndexView,
    meta:{
      requestAuth:true,
    }
  },
  {
    path:"/user/account/login/",
    name:"user_account_login",
    component:UserAccountLoginView,
    meta: {
      hideNavBar: true ,
      requestAuth:false,
    }
  },
  {
    path:"/user/account/register/",
    name:"user_account_register",
    component:UserAccountRegisterView,
    meta: {
      hideNavBar: true ,
      requestAuth:false,
    }
  },
  {
    path:"/404/",
    name:"404",
    component:NotFound,
    meta:{
      requestAuth:true,
    }
  },
  {
    path:"/:catchAll(.*)",
    redirect:"/404/",
  },
]

另外 需要判断当前是否在登入状态

import store from "@/store/index"

router.beforeEach((to, from, next) => {
  if (to.meta.requestAuth && !store.state.user.is_login) {
    next({ name: 'user_account_login' });
  } else {
    next();
  }
});

测试可以发现 退出登入会直接重定向到登入页面 哪怕直接输入网址也会直接给你退出来

实现注册功能 及完善细节

<template>
  <div class="container" :class="{ 'sign-up-mode': isSignUpMode }">
    <div class="forms-container">
      <div class="signin-signup">

        
          <h2 class="title">登录</h2>
          <div class="input-field">
            <i class="fas fa-user"></i>
            <input type="text" v-model="username" placeholder="用户名" required />
          </div>
          <div class="input-field">
            <i class="fas fa-lock"></i>
            <input type="password" v-model="password" placeholder="密码" required />
          </div>
          <div class="error-message" >{{error_message}}</div>
          <input type="submit" value="立即登录" class="btn solid" />
          <p class="social-text">通过其他方式</p>
          <div class="social-media">
            <a href="#" class="social-icon"><i class="fab fa-qq"></i></a>
            <a href="#" class="social-icon"><i class="fab fa-weixin"></i></a>
            <a href="#" class="social-icon"><i class="fab fa-weibo"></i></a>
            <a href="#" class="social-icon"><i class="fab fa-alipay"></i></a>
          </div>
        

        
          <h2 class="title">注册</h2>
          <div class="input-field">
            <i class="fas fa-user"></i>
            <input type="text" v-model="registerUsername" placeholder="用户ID" required />
          </div>
          <div class="input-field">
            <i class="fas fa-lock"></i>
            <input type="password" v-model="registerPassword" placeholder="密码" required />
          </div>
          <div class="input-field">
            <i class="fas fa-lock"></i>
            <input type="password" v-model="confirmPassword" placeholder="确认密码" required />
          </div>
          <div class="error-message">{{ registerErrorMessage }}</div>
          <input type="submit" class="btn" value="立即注册" />
          <p class="social-text">通过其他方式</p>
          <div class="social-media">
            <a href="#" class="social-icon"><i class="fab fa-qq"></i></a>
            <a href="#" class="social-icon"><i class="fab fa-weixin"></i></a>
            <a href="#" class="social-icon"><i class="fab fa-weibo"></i></a>
            <a href="#" class="social-icon"><i class="fab fa-alipay"></i></a>
          </div>
        
      </div>
    </div>
    <div class="panels-container">
      <div class="panel left-panel">
        <div class="content">
          <h3>加入我们</h3>
          <p>加入我们,成为本站的一份子。</p>
          <button class="btn transparent" @click="toggleSignUp">去注册</button>
        </div>
        <img src="api/images/iqk9G8gjTAMx/panel-1.svg" class="image" alt="" />
      </div>
      <div class="panel right-panel">
        <div class="content">
          <h3>已有帐号?</h3>
          <p>立即登录已有帐号,享受独家权益。</p>
          <button class="btn transparent" @click="toggleSignIn">去登录</button>
        </div>
        <img src="api/images/rXGAQqjoS1wv/panel-2.svg" class="image" alt="" />
      </div>
    </div>
  </div>
</template>


import { useStore } from "vuex";
import { ref } from 'vue';
import router from "@/router/index";
import $ from 'jquery';
import Swal from "sweetalert2";

export default {
  setup() {
    const store = useStore();
    const isSignUpMode = ref(false);
    const username = ref("");
    const password = ref("");
    const error_message = ref("");

    const registerUsername = ref("");
    const registerPassword = ref("");
    const confirmPassword = ref("");
    const registerErrorMessage = ref("");

    const toggleSignUp = () => {
      isSignUpMode.value = true;
    };
    const toggleSignIn = () => {
      isSignUpMode.value = false;
    };

    const login = () => {
      error_message.value = "";

      store.dispatch("login", {
        username: username.value,
        password: password.value,
        success: () => {
          // 使用 sweetalert2 显示欢迎提示
          Swal.fire({
            title: "欢迎!",
            text: `欢迎回来,${username.value}!`,
            icon: "success",
            confirmButtonText: "确定"
          }).then(() => {
            // 确保获取最新的用户信息
            store.dispatch("getinfo", {
              success: () => {
                // 登录成功后跳转到主页
                router.push({ name: "home" });
              },
              error: () => {
                error_message.value = "获取用户信息失败,请重新登录";
              }
            });
          });
        },
        error: (resp) => {
          if (resp.error_message === "用户名不存在") {
            error_message.value = "用户名不存在,请去注册账号";
          } else if (resp.error_message === "密码错误") {
            error_message.value = "密码错误,请重新输入";
          } else {
            error_message.value = "登录失败,请稍后再试";
          }
        },
      });
    };

    const register = () => {
      registerErrorMessage.value = "";
      $.ajax({
        url: "http://localhost:3000/user/account/register/",
        type: "post",
        data: {
          username: registerUsername.value,
          password: registerPassword.value,
          confirmedPassword: confirmPassword.value,
        },
        success(resp) {
          if (resp.error_message === "success") {
            Swal.fire({
              title: "注册成功!",
              text: "请登录您的账号。",
              icon: "success",
              confirmButtonText: "确定"
            }).then(() => {
              isSignUpMode.value = false;
              router.push({ name: "user_account_login" });
            });
          } else {
            registerErrorMessage.value = resp.error_message;
          }
        },
        error() {
          registerErrorMessage.value = "注册失败,请稍后再试。";
        },
      });
    };

    return {
      isSignUpMode,
      username,
      password,
      error_message,
      registerUsername,
      registerPassword,
      confirmPassword,
      registerErrorMessage,
      toggleSignUp,
      toggleSignIn,
      login,
      register,
    };
  },
};




.error-message {
  color: red;
  font-size: 14px;
  margin-top: 10px;
}

@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500;600;700;800&display=swap");

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body,
input {
  font-family: "Poppins", sans-serif;
}

.container {
  position: relative;
  max-width: 100vw;
  background-color: #fff;
  min-height: 100vh;
  overflow: hidden;
}

.forms-container {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}

.signin-signup {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  left: 75%;
  width: 50%;
  transition: 1s 0.7s ease-in-out;
  display: grid;
  grid-template-columns: 1fr;
  z-index: 5;
}

form {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  padding: 0rem 5rem;
  transition: all 0.2s 0.7s;
  overflow: hidden;
  grid-column: 1 / 2;
  grid-row: 1 / 2;
}

form.sign-up-form {
  opacity: 0;
  z-index: 1;
}

form.sign-in-form {
  z-index: 2;
}

.title {
  font-size: 2.2rem;
  color: #444;
  margin-bottom: 10px;
}

.input-field {
  max-width: 380px;
  width: 100%;
  background-color: #f0f0f0;
  margin: 10px 0;
  height: 55px;
  border-radius: 55px;
  display: grid;
  grid-template-columns: 15% 85%;
  padding: 0 0.4rem;
  position: relative;
}

.input-field i {
  text-align: center;
  line-height: 55px;
  color: #acacac;
  transition: 0.5s;
  font-size: 1.1rem;
}

.input-field input {
  background: none;
  outline: none;
  border: none;
  line-height: 1;
  font-weight: 600;
  font-size: 1.1rem;
  color: #333;
}

.input-field input::placeholder {
  color: #aaa;
  font-weight: 500;
}

.social-text {
  padding: 0.7rem 0;
  font-size: 1rem;
}

.social-media {
  display: flex;
  justify-content: center;
}

.social-icon {
  height: 46px;
  width: 46px;
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0 0.45rem;
  color: #333;
  border-radius: 50%;
  border: 1px solid #333;
  text-decoration: none;
  font-size: 1.1rem;
  transition: 0.3s;
}

.social-icon:hover {
  color: #4481eb;
  border-color: #4481eb;
}

.btn {
  width: 150px;
  background-color: #5995fd;
  border: none;
  outline: none;
  height: 49px;
  border-radius: 49px;
  color: #fff;
  text-transform: uppercase;
  font-weight: 600;
  margin: 10px 0;
  cursor: pointer;
  transition: 0.5s;
}

.btn:hover {
  background-color: #4d84e2;
}
.panels-container {
  position: absolute;
  height: 100%;
  width: 100%;
  top: 0;
  left: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}

.container:before {
  content: "";
  position: absolute;
  height: 2000px;
  width: 2000px;
  top: -10%;
  right: 48%;
  transform: translateY(-50%);
  background-image: linear-gradient(-45deg, #4481eb 0%, #04befe 100%);
  transition: 1.8s ease-in-out;
  border-radius: 50%;
  z-index: 6;
}

.image {
  width: 100%;
  transition: transform 1.1s ease-in-out;
  transition-delay: 0.4s;
}

.panel {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: space-around;
  text-align: center;
  z-index: 6;
}

.left-panel {
  pointer-events: all;
  padding: 3rem 17% 2rem 12%;
}

.right-panel {
  pointer-events: none;
  padding: 3rem 12% 2rem 17%;
}

.panel .content {
  color: #fff;
  transition: transform 0.9s ease-in-out;
  transition-delay: 0.6s;
}

.panel h3 {
  font-weight: 600;
  line-height: 1;
  font-size: 1.5rem;
}

.panel p {
  font-size: 0.95rem;
  padding: 0.7rem 0;
}

.btn.transparent {
  margin: 0;
  background: none;
  border: 2px solid #fff;
  width: 130px;
  height: 41px;
  font-weight: 600;
  font-size: 0.8rem;
}

.right-panel .image,
.right-panel .content {
  transform: translateX(800px);
}

/* ANIMATION */

.container.sign-up-mode:before {
  transform: translate(100%, -50%);
  right: 52%;
}

.container.sign-up-mode .left-panel .image,
.container.sign-up-mode .left-panel .content {
  transform: translateX(-800px);
}

.container.sign-up-mode .signin-signup {
  left: 25%;
}

.container.sign-up-mode form.sign-up-form {
  opacity: 1;
  z-index: 2;
}

.container.sign-up-mode form.sign-in-form {
  opacity: 0;
  z-index: 1;
}

.container.sign-up-mode .right-panel .image,
.container.sign-up-mode .right-panel .content {
  transform: translateX(0%);
}

.container.sign-up-mode .left-panel {
  pointer-events: none;
}

.container.sign-up-mode .right-panel {
  pointer-events: all;
}

@media (max-width: 870px) {
  .container {
    min-height: 800px;
    height: 100vh;
  }
  .signin-signup {
    width: 100%;
    top: 95%;
    transform: translate(-50%, -100%);
    transition: 1s 0.8s ease-in-out;
  }

  .signin-signup,
  .container.sign-up-mode .signin-signup {
    left: 50%;
  }

  .panels-container {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr 2fr 1fr;
  }

  .panel {
    flex-direction: row;
    justify-content: space-around;
    align-items: center;
    padding: 2.5rem 8%;
    grid-column: 1 / 2;
  }

  .right-panel {
    grid-row: 3 / 4;
  }

  .left-panel {
    grid-row: 1 / 2;
  }

  .image {
    width: 200px;
    transition: transform 0.9s ease-in-out;
    transition-delay: 0.6s;
  }

  .panel .content {
    padding-right: 15%;
    transition: transform 0.9s ease-in-out;
    transition-delay: 0.8s;
  }

  .panel h3 {
    font-size: 1.2rem;
  }

  .panel p {
    font-size: 0.7rem;
    padding: 0.5rem 0;
  }

  .btn.transparent {
    width: 110px;
    height: 35px;
    font-size: 0.7rem;
  }

  .container:before {
    width: 1500px;
    height: 1500px;
    transform: translateX(-50%);
    left: 30%;
    bottom: 68%;
    right: initial;
    top: initial;
    transition: 2s ease-in-out;
  }

  .container.sign-up-mode:before {
    transform: translate(-50%, 100%);
    bottom: 32%;
    right: initial;
  }

  .container.sign-up-mode .left-panel .image,
  .container.sign-up-mode .left-panel .content {
    transform: translateY(-300px);
  }

  .container.sign-up-mode .right-panel .image,
  .container.sign-up-mode .right-panel .content {
    transform: translateY(0px);
  }

  .right-panel .image,
  .right-panel .content {
    transform: translateY(300px);
  }

  .container.sign-up-mode .signin-signup {
    top: 5%;
    transform: translate(-50%, 0);
  }
}

@media (max-width: 570px) {
  form {
    padding: 0 1.5rem;
  }

  .image {
    display: none;
  }
  .panel .content {
    padding: 0.5rem 1rem;
  }
  .container {
    padding: 1.5rem;
  }

  .container:before {
    bottom: 72%;
    left: 50%;
  }

  .container.sign-up-mode:before {
    bottom: 28%;
    left: 50%;
  }
}


PixPin_2024-10-31_06-27-56-69c6ec27

实现登入状态持久化

现在还有个问题 登入后刷新浏览器 就会丢失登入状态 这是因为现在的token是存在浏览器缓存里的 要想持久化得想办法存在浏览器的一小块硬盘空间localStorge中

在web/src/store/User.js 中

login函数成功后 localStorage.setItem("jwt_token",resp.token);

image-ad5b041b

logout成功后 localStorage.removeItem("jwt_token");

image-61421167

打开浏览器 登入账号 调试看application--local storage 中 多了一条jwt_token

image-c159be26

说明已经存成功了 但是刷新还是会退出登入状态

这是每次刷新页面时 它不会自动从我们的localstorage中取出token

需要我们写个逻辑 如果重定向在了登入注册页面 就去里面找token 如果有且没过期就无需重新登入

image-353649fe
const jwt_token = localStorage.getItem("jwt_token");
    if(jwt_token) {
      store.commit("updateToken",jwt_token);
      store.dispatch("getinfo",{
        success(){
            router.push({name:"home"});
        },
        error(){

        }
      })
    }

实际就是做了一遍登录里的逻辑 刷新会先在登入界面 用本地存的与后端验证 成功了就跳到主页

可以发现刷新的时候 登录界面会一闪而过

处理一闪而过

这里可以优化ui 还是用v-if 默认不展示 当验证成功后才展示

image-44b54680 image-9cf55e26 image-c49982c5

或者用一种更全面的方式 在拉取信息时 这些组件都不展示 就可以避免一闪而过的情况

image-62b757a7 image-fd9db5bc image-b2b7ee7b

使用这种全局的方式 其他组件也可以应用 Narvar优化

整体优化

简单优化了项目结构 与后端交互api的封装 全局配置信息

一些页面上的细节

尚未完成的点可能就是 鼠标悬浮显示密码 可能因为被原css覆盖的原因 不知道为什么实现不了 以后再看

https://git.acwing.com/Zwww/kob/-/commit/948eb542c186f0cf93cd4528df238df6f81af38f


项目分区导航项目构建工具 ⬅️ | 00-业务具体实现 | ➡️ Http请求 以及 Springboot注解