--- title: "00-业务具体实现" created: 2025-12-02 tags: - 项目 aliases: - 业务具体实现 --- # 业务具体实现 ## 分析 开始前把逻辑理清楚 所有的页面大概分为两大类——公开页面(login等)和授权页面(/) 授权是怎么实现的 前面我们使用的是sessionid 用户登入时 会查询数据库中该用户的信息 将用户输入的用户名和密码与数据库存储的用户名和密码进行比对(有加密) 如果一致 会给用户发送一个sessionid(类似于身份证号) 用户收到id会把它存在本地(cookie)之后每次在访问授权页面时 都会将存储的这个id发给服务器 服务端也会存下它(或是缓存或是数据库) 服务器会判断这个seesion是否有效 若有效 则从服务器端对应的controller信息提取填入上下文当中 现在换一种验证方式 传统的session的模式不是很方便 如果涉及到跨域就很蛋疼 现在有种流行的jwt验证方式 它可以很容易的实现跨域 也不需要在服务器端存储 以一个令牌可以登入多个服务器 原理也差不多 一个服务器登入后 不再是给session 而是给一个token 服务器给了用户一个token后 自己是不用存任何东西的 那怎么验证身份呢 它将userid或者一些用户信息 加在密钥里面 大概可以看成两部分组成 用户信息+服务器也没存储的密钥 然后通过一些特定的加密算法 把这个字符串映射成新字符串 ![[image-f9089aad.png]] 之后 将userid那一段和新生成的那段 拼接起来 返回给用户 作为jwt-token ![[image-a5049137.png]] 未来服务器验证也很简单 把userid加上密钥 再用这种加密算法算一遍 如果结果一样 即验证成功 具体来看:[[03-JWT验证|JWT验证]] 那么抽象一下 首先通过登入页面 获取一个token 用户得到token后 存在浏览器里面 每次发送请求时 都会带上token 服务器端接收到请求 发现如果是受限链接 就会验证这个token 如果合法 就根据这个token 里面的用户信息 从数据库查找出对应user的内容 提取到上下文之中 然后才可以访问需要授权的页面 这里提供几个工具类 [[07-utils.JwtUtil类|utils.JwtUtil类]] [[06-config.filter.JwtAuthenticationTokenFilter类|config.filter.JwtAuthenticationTokenFilter类]] [[05-config.SecurityConfig类|config.SecurityConfig类]] ## 引入依赖 - jjwt-api - jjwt-impl - jjwt-jackson [Maven仓库地址](https://mvnrepository.com/) 现在不敢乱加新版本了 用chatgpt推荐的对应springboot3.1.1的老版本 ```xml io.jsonwebtoken jjwt-api 0.11.5 io.jsonwebtoken jjwt-impl 0.11.5 runtime io.jsonwebtoken jjwt-jackson 0.11.5 runtime ``` ## 工具类 ### utils.JwtUtil **功能** 1、将一个字符串 加上密钥 加上有效期 把他变成一个加密后的字符串 2、得到一个token 从中解析出userid 在backend包下建utils包(不知道放哪的文件就放在这个工具包下) 新建JwtUtil类 把前面的给的代码复制进去 ![[image-5ee07a2e.png]] ```java 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; 爆红问题 ```xml org.jetbrains annotations 24.0.0 ``` 这俩不清楚要不要加 后面调了蛮久发现没用到 ```xml javax.servlet javax.servlet-api 4.0.1 jakarta.servlet jakarta.servlet-api 5.0.0 provided ``` ![[image-663fd860.png]] 改后得到的代码 ```java 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.png]] ```java 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`: ```java private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) { this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter; } ``` 这部分代码通过构造函数注入 `JwtAuthenticationTokenFilter`,这是自定义的 JWT 身份验证过滤器,用于处理 JWT 令牌的解析和验证。 `PasswordEncoder` **Bean**: ```java @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } ``` 配置一个 `PasswordEncoder` Bean,采用 `BCryptPasswordEncoder` 来对密码进行加密。这个加密器可以确保用户密码存储的安全性。 `AuthenticationManager` **Bean**: ```java @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } ``` 定义一个 `AuthenticationManager`,这个 Bean 用于处理用户认证操作。`AuthenticationConfiguration` 提供了默认的认证管理器配置。 `SecurityFilterChain` **配置**: ```java @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.png]] 这部分添加的链接 表示为公开的 无需验证 之后需要加哪个页面 允许公开访问 就只需要在这加入链接 用逗号隔开 ## 创建后端api ### 准备工作 #### 修改表结构 修改id为主键、唯一、非null、自增 ![[image-a5855085.png]] 新增列photo 存储用户头像 因为存的是链接可能比较长 设置为varchar(1000) ![[image-eac235eb.png]] 得到: ![[image-bc004061.png]] 暂时用acwing的头像 之后这些东西可以搭一个图床 #### 配置Pojo backend/pojo/User.java ```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.png]] 也一般是以板块来分 这是user板块的接口 所以在service包下建一个user包 然后该包下可能有很多板块 账号啊 机器人啊什么的 这三个属于账号相关的 再给它封装在account包下 接口的数据一般习惯使用map返回 InfoService: ```java package com.zwnsyw.backend.service.user.account; import java.util.Map; public interface InfoService { public Map getInfo(); } ``` LoginService: ```java package com.zwnsyw.backend.service.user.account; import java.util.Map; public interface LoginService { public Map getToken(String username, String password); } ``` RegisterService: ```java package com.zwnsyw.backend.service.user.account; import java.util.Map; public interface RegisterService { public Map register(String username, String password, String confirmedPassword); } ``` #### service/impl 具体实现 ##### LoginServiceImpl ![[image-5bc25db7.png]] 加上@Service注解说明是Service implements LoginService 说明实现该接口 alt+insert 实现方法 得到 ```java 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 getToken(String username, String password) { return Map.of(); } } ``` 登入的话需要先验证用户是否登入成功 需要用到AuthenticationManager 把它注入进来 ```java @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`中返回。 ```java 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 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 map=new HashMap<>(); map.put("error_message","success"); map.put("token",jwt); return map; } } ``` ##### InfoServiceImpl ![[image-4824ced4.png]] 老规矩 加@Service 、实现自InfoService 、alt + insert 实现方法 ```java 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 getInfo() { UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); UserDetailsImpl loginUser = (UserDetailsImpl) authentication.getPrincipal(); User user = loginUser.getUser(); Map 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`,即当前登录的用户信息,这里将其强制类型转换为`UserDetailsImpl`。`UserDetailsImpl`是自定义的类,实现了`UserDetails`接口,包含了用户的具体信息。 - 通过`UserDetailsImpl`的`getUser`方法获取到`User`对象,这是用户信息的实体类。 - 创建一个`HashMap`来存储用户信息,包括一个固定的`error_message`字段(这里设置为"success"表示操作成功),以及用户的ID、用户名和照片。 - 最后,返回这个包含用户信息的`Map`。 ##### RegisterServiceImpl 写一系列判断逻辑 验证注册时的账号密码是否合法 其中 是否重名需要用到查询数据库 注入userMapper 存储时加密密码 需要注入PasswordEncoder ```java 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 register(String username, String password, String confirmedPassword) { Map 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 queryWrapper = new QueryWrapper(); queryWrapper.eq("username",username); List 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.png]] 标明为@RestController 再将刚刚定义的接口注入进来 ![[image-fe5df582.png]] 登入应该是一个post请求 (用户名密码) get请求是url链接 明文传输 不安全 而post看不到明文 ![[image-6070b454.png]] 注意设置的入口需要在SecurityConfig中放行出来 ![[image-08d5bc54.png]] 使用@RequestParam把请求参数绑定下来 得到代码: ```java 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 getToken(@RequestParam Map map){ // String username=map.get("username"); // String password=map.get("password"); // return loginService.getToken(username,password); // } @PostMapping("/user/account/token/") public Map getToken(@RequestParam String username, @RequestParam String password){ return loginService.getToken(username,password); } } ``` ###### 插曲 (提醒) 工具postman 打开调试发现 403 ![[image-f7b9c0c8.png]] 这是因为 我们出于安全 写的是一个post请求 而浏览器访问给的是一个get请求 所以不能直接在浏览器里调 现在有两种选择 一个是使用工具 postman 另一个是直接用前端连上调 这里没讲第一种 但感觉还是要学这个工具的 以后总不可能全是全栈的项目吧 使用ajax调试 ```javascript 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.png]] ##### InfoController ![[image-331a00a2.png]] 标明为@RestController 注入InfoService 这里是得到一个信息 使用GetMapping ```java 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 getinfo(){ return infoService.getInfo(); } } ``` 在index.vue中 使用ajax发送一个get请求 get请求的入口在backend/config/filter/JwtAuthenticationTokenFilter.java中 一般为Authorization ![[image-d6f2d1bd.png]] 把先前得到的token 使用Bearer+token的形式进行认证 注意 Bearer后一定要跟个空格 也是因为JwtAuthenticationTokenFilter中的配置 ![[image-acd6d3be.png]] App.vue中如下: ```javascript $.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.png]] ##### RegisterController ![[image-bc41f5ca.png]] 注册也是个post请求 因为密码不能明文传 从前端获取三个参数传给接口 记得在SecurityConfig中放行 ![[image-575662c6.png]] ```java 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 register(@RequestParam Map map){ String username = map.get("username"); String password = map.get("password"); String confirmedPassword = map.get("confirmedPassword"); return registerService.register(username,password,confirmedPassword); } } ``` 在前端进行调试 ```javascript $.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.png]] 至此登入注册模块的后端部分基本写完 ### 思考拓展 前面的内容频繁接触到了一些东西 比如get post 请求 有关密码的请求都得用post请求 因为它不和get一样用url式的明文传输 更加安全 调试上 用到了ajax的一些方法 其实之前或多或少也见过 但没特别去学这个用法 还有一种不在前端调试的方法 使用postman工具 现在也拓展一下 以及@Autowired注解 用起来感觉像是 导包 就是要用到哪部分功能 就给他注入当前工作域 然后@Service @RestController @Mapper 等注解就是说明是哪一部分内容 方便springboot找到并管理 @RequestParam注解就是把网页传来的东西绑定在java的某个变量上 用Map存的话 就不用写那么长的参数 ![[image-df5e8163.png]] 你的理解是正确的。你提到的内容中,关于GET和POST请求的差异、安全性,以及在需要传输敏感数据时使用POST请求,这是Web开发中的一个关键概念。GET请求的参数是通过URL明文传输的,而POST请求则是将参数包含在请求体中,因此在处理敏感信息时更加安全。 对于AJAX的使用和Postman工具的调试,你已经掌握了基本的调试方法。AJAX用于在不刷新页面的情况下与服务器通信,而Postman则可以帮助你快速调试和测试API请求。 @Autowired注解确实是一种依赖注入(Dependency Injection)的方式,可以自动将需要的组件注入到当前类中,简化了对象的创建和管理。而@Service、@RestController、@Mapper等注解的确起到了标识作用,它们告诉Spring Boot这些类的具体用途,让框架能更好地进行管理。 至于@RequestParam注解,你也理解得很到位。它能够将HTTP请求的参数自动绑定到方法参数中,使用Map时可以简化代码。 [[01-Http请求 以及 Springboot注解|Http请求 以及 Springboot注解]] [[04-前端请求工具(HTTP请求库)|前端请求工具(HTTP请求库)]] [[02-Postman|Postman]] ## 前端代码编写 ![[image-4114c2cd.png]] 记得在router中加入这俩页面 ```javascript { path:"/user/account/login/", name:"user_account_login", component:UserAccountLoginView, }, { path:"/user/account/register/", name:"user_account_register", component:UserAccountRegisterView, }, ``` ![[image-83680629.png]] ![[image-3a6c27a6.png]] ### 登录页面 #### 大概样式实现 bootstrip中找 ```html 用户名 密码 密码错误 提交 ``` ![[image-ae31f34a.png]] #### 全局信息和登入请求 另外 我们需要存储一些全局信息 在每个页面都需要存一下当前登入的用户是谁 所以需要把用户信息存在全局的 这里需要用到vuex 用户相关的信息 存在一个新的模块里面 ![[image-d1f2bcef.png]] vue存全局信息一般都是存store/index.js里面的这几个东西 ![[image-a0029aeb.png]] 直接给他复制到User,js里面去 定义好要用的变量和set接口 ```javascript 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: ```javascript import { createStore } from 'vuex' import ModuleUser from './User' export default createStore({ state: { }, getters: { }, mutations: { }, actions: { }, modules: { user:ModuleUser, } }) ``` 接下来实现登入 其实就是调试时的post请求 现在的用户名密码是传入的而非手写的 如果成功请求 就把得到的token写入全局 store/User.js ```javascript 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中应用登入逻辑 ```javascript 用户名 密码 {{error_message}} 提交 ``` ![[image-98c47fe8.png]] ![[image-46d93ce0.png]] 如果验证成功 就跳转到主页面 ```javascript 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.gif]] #### 获取用户信息 登入成功后 需要动态的显示出我们的信息 如昵称 头像等等 所以在登录完后 还要向后端发送请求 获取用户信息 在store/user.js中再写一个辅助函数 其实就是调试infoController时做的get请求 User,js ```javascript 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中调用 ```javascript 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.png]] 成功 #### 应用到导航栏 获取到这些信息后 给展示到页面上 用v-if v-else 进行渲染 如果登入成功 就展示个人信息 否则展示登录注册按钮 ```html {{$store.state.user.username}} 我的Bot 退出 登录 注册 ``` #### 退出登录 前面的认证机制可以发现 token完全存在用户本地 要让认证失败 直接本地把token删了即可 所以这一步不需要后端的介入 (其实现在有个bug 因为token存在浏览器内存里面 只要一刷新 登入就失效了 后续会存在localstorage里面 即使关闭再打开也不会失效) 在user,js的mutations中写一个退出的修改状态函数 ```javascript 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中写个接口 ```javascript logout(context){ context.commit("logout"); } ``` User.js: ```javascript 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里加一个函数 ```javascript import { useStore} from "vuex"; export default{ setup(){ const store = useStore(); const logout = () => { store.dispatch("logout"); } return{ logout } } } ``` 然后给退出按钮绑定事件 ```html 退出 ``` 完整NavBar.vue如下 ```javascript King Of Bots 对战 对局列表 排行榜 {{$store.state.user.username}} 我的Bot 退出 登录 注册 ``` ![[PixPin_2024-10-31_03-53-29-df31ff54.gif]] 切换页面不掉登录状态 用户登录右上角显示自己的账号 退出登入 已经实现 发现还有很多问题需要完善 比如登入如果账号不存在应该提示不存在该账号而不是用户名或密码错误 未登录状态还可以访问其他页面 这也是不合理的 应该加上权限控制 ### 注册逻辑 #### 我的修改 我给修改成了注册登入在同一个模块 如果没授权时 默认重定向到这个注册登入页面 它虽然只有一个页面但可以切换成登入或是注册 然后授权成功后 可以访问网站里的资源 并把自己的用户信息填充在导航栏的右上角 比如头像昵称等等 然后有个退出登入按钮 退出后直接又到了这个注册登入页面 ![[PixPin_2024-10-31_05-01-04-a23c85c5.gif]] 所以后面可能不是跟着y总做 但其实变化也不大 前端只不过是样式 样式写在一个文件里和写在两个文件里 没什么大区别 注意关注y总的注册逻辑实现就好了 可以看到这个页面其实是没有导航栏的 但是导航栏组件是用在了App.vue 按道理全部页面都会有 这个是怎么删掉的 其实也是路由配置 和 类似vue-if的渲染问题 在路由配置里加上一句 `meta: { hideNavBar: true }` ```javascript { path:"/user/account/login/", name:"user_account_login", component:UserAccountLoginView, meta: { hideNavBar: true } }, ``` App.vue中 将 改成 即可 另外 在vue项目中 要引入什么图标库 或者其他什么东西 都得用npm 比如这里引入一些图标 ```bash npm install @fortawesome/fontawesome-free ``` 然后在App.vue中引入 ```javascript import '@fortawesome/fontawesome-free/css/all.css'; import '@fortawesome/fontawesome-free/js/all.js'; ``` #### 实现前端页面的授权 就是有些页面只能登入后才能看 而有些不登录就能看 如果没登录而进入了需要授权的页面 就会重定向回某个设定的页面 这可以通过路由守卫来完成 `router.beforeEach((to, from, next)` to:跳转到哪个页面 from:从哪个页面跳转而来 next:页面要不要执行下一步操作 在这之前就需要标记每一个页面是不是授权页面 ```javascript 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/", }, ] ``` 另外 需要判断当前是否在登入状态 ```javascript 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(); } }); ``` 测试可以发现 退出登入会直接重定向到登入页面 哪怕直接输入网址也会直接给你退出来 #### 实现注册功能 及完善细节 ```html 登录 {{error_message}} 通过其他方式 注册 {{ registerErrorMessage }} 通过其他方式 加入我们 加入我们,成为本站的一份子。 去注册 已有帐号? 立即登录已有帐号,享受独家权益。 去登录 ``` ![[PixPin_2024-10-31_06-27-56-69c6ec27.gif]] #### 实现登入状态持久化 现在还有个问题 登入后刷新浏览器 就会丢失登入状态 这是因为现在的token是存在浏览器缓存里的 要想持久化得想办法存在浏览器的一小块硬盘空间localStorge中 在web/src/store/User.js 中 login函数成功后 `localStorage.setItem("jwt_token",resp.token);` ![[image-ad5b041b.png]] logout成功后 `localStorage.removeItem("jwt_token");` ![[image-61421167.png]] 打开浏览器 登入账号 调试看application--local storage 中 多了一条jwt\_token ![[image-c159be26.png]] 说明已经存成功了 但是刷新还是会退出登入状态 这是每次刷新页面时 它不会自动从我们的localstorage中取出token 需要我们写个逻辑 如果重定向在了登入注册页面 就去里面找token 如果有且没过期就无需重新登入 ![[image-353649fe.png]] ```javascript 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.png]] ![[image-9cf55e26.png]] ![[image-c49982c5.png]] 或者用一种更全面的方式 在拉取信息时 这些组件都不展示 就可以避免一闪而过的情况 ![[image-62b757a7.png]] ![[image-fd9db5bc.png]] ![[image-b2b7ee7b.png]] 使用这种全局的方式 其他组件也可以应用 Narvar优化 ### 整体优化 简单优化了项目结构 与后端交互api的封装 全局配置信息 一些页面上的细节 尚未完成的点可能就是 鼠标悬浮显示密码 可能因为被原css覆盖的原因 不知道为什么实现不了 以后再看 --- **项目分区导航**: [[01-项目构建工具|项目构建工具]] ⬅️ | 00-业务具体实现 | ➡️ [[01-Http请求 以及 Springboot注解|Http请求 以及 Springboot注解]]
通过其他方式
加入我们,成为本站的一份子。
立即登录已有帐号,享受独家权益。