0%

AcWing SpringBoot项目实战03

学习平台

AcWing SpringBoot框架课

环境配置

安装配置MySQL

这里安装配置MySQL以及设置用户名密码就不再赘述了。

配置SpringBoot

  1. maven仓库地址

  2. MyBatis-Plus官网

  3. 在后端项目的pom.xml中导入依赖:

    • Spring Boot Starter JDBC
    • Project Lombok
    • MySQL Connector/J
    • mybatis-plus-boot-starter
    • mybatis-plus-generator
    • spring-boot-starter-security
    • jjwt-api
    • jjwt-impl
    • jjwt-jackson
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    <version>2.7.1</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.2</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.3</version>
    </dependency>
  4. application.properties中添加数据库配置

    1
    2
    3
    4
    spring.datasource.username=root
    spring.datasource.password=123456
    spring.datasource.url=jdbc:mysql://localhost:3306/kob?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  5. SpringBoot中的常用模块

    • pojo层:将数据库中的表对应成Java中的Class
    • mapper层(也叫Dao层):将pojo层的class中的操作,映射成sql语句
    • service层:写具体的业务逻辑,组合使用mapper中的操作
    • controller层:负责请求转发,接受页面过来的参数,传给Service处理,接到返回值,再传给页面

配置Spring Security

Spring Security可以实现页面拦截效果,以前的项目大多需要编写一个过滤器,才能实现一些页面的拦截。

实现步骤

  1. 导入Spring Security的依赖包

    1
    2
    3
    4
    5
    6
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.7.1</version>
    </dependency>
  2. 实现service.impl.UserDetailsServiceImpl类,继承自UserDetailsService接口,用来接入数据库信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package com.kob.backend.service.impl;

    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.kob.backend.mapper.UserMapper;
    import com.kob.backend.pojo.User;
    import com.kob.backend.service.impl.utils.UserDetailImpl;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;

    /**
    * @author xzt
    * @version 1.0
    */
    @Service
    public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    /**
    * 通过传过来的用户名来返回对应的用户的详细信息
    * @param username 用户名
    * @return 对应的用户的详细信息
    * @throws UsernameNotFoundException
    */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    User user = userMapper.selectOne(queryWrapper);
    if(user == null) throw new RuntimeException("用户不存在");
    return new UserDetailsImpl(user); // 需要编写UserDetailImpl来实现UserDetails接口
    }
    }

  3. 实现service.impl.utils.UserDetailImpl实现UserDetails接口。用来返回查询到的用户相关信息.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    package com.kob.backend.service.impl.utils;

    import com.kob.backend.pojo.User;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;

    import java.util.Collection;

    /**
    * @author xzt
    * @version 1.0
    */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class UserDetailsImpl implements UserDetails {
    private User user;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    return null;
    }

    @Override
    public String getPassword() {
    return user.getPassword();
    }

    @Override
    public String getUsername() {
    return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
    return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    return true;
    }

    @Override
    public boolean isEnabled() {
    return true;
    }
    }

注意:如果数据库中的密码没有加密,并且想要进行登录验证,则需要在数据库中的密码前面加上{noop}

实现密码加密处理

实现config.SecurityConfig类,用来实现用户密码的加密存储。

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

使用的是BCryptPasswordEncoder()进行加密,主要有如下几个方法:

  • encode(password) 可以将密码明文转为密文,每次加密之后的结果不一样
  • matches(password, encodedPassword) 判断明文和密文是否匹配.

集成JWT验证

在基于令牌的身份验证中,我们使用JWT(JSON Web Tokens)进行身份验证。

传统的登录验证模式:使用session来进行判断.不能解决跨域判断.

image-20230313154046090

JWT验证模式

image-20230313160056635

实现步骤

  1. 导入Jwt相关依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
    </dependency>
  1. 实现utils.JwtUtil类,为jwt工具类,用来创建、解析jwt token

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    package com.kob.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;

    /**
    * @author xzt
    * @version 1.0
    * 用来创建、解析jwt
    */
    @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();
    }
    }
  2. 实现config.filter.JwtAuthenticationTokenFilter类,用来验证jwt token,如果验证成功,则将User信息注入上下文中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    package com.kob.backend.filter;

    import com.kob.backend.mapper.UserMapper;
    import com.kob.backend.pojo.User;
    import com.kob.backend.service.impl.utils.UserDetailsImpl;
    import com.kob.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.util.StringUtils;
    import org.springframework.web.filter.OncePerRequestFilter;

    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;

    /**
    * @author xzt
    * @version 1.0
    */
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private UserMapper userMapper;

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

    if (!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);
    }
    }
  3. 配置config.SecurityConfig类,放行登录、注册等接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    package com.kob.backend.config;

    import com.kob.backend.filter.JwtAuthenticationTokenFilter;
    import org.springframework.beans.factory.annotation.Autowired;
    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.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

    /**
    * @author xzt
    * @version 1.0
    * 实现用户密码的加密存储, 放行登录,注册等接口
    */
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

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

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and()
    .authorizeRequests()
    .antMatchers("/user/account/token/", "/user/account/register/").permitAll()
    .antMatchers(HttpMethod.OPTIONS).permitAll()
    .anyRequest().authenticated();

    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    }

数据库修改

将数据库中的id域变为自增

  • 在数据库中将id列变为自增
  • pojo.User类中添加注解:@TableId(type = IdType.AUTO)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.kob.backend.pojo;

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

/**
* @author xzt
* @version 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@TableId(type = IdType.AUTO)
private Integer id;

private String username;

private String password;

}

后端实现

编写API需要分别在controllerServiceMapper 实现.这里主要编写了注册、登录、以及从jwt token中获取用户信息。

登录功能

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

  1. 实现service/user/account/LoginService

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.kob.backend.service.user.account;

    import org.springframework.stereotype.Service;

    import java.util.Map;

    /**
    * @author xzt
    * @version 1.0
    */
    public interface LoginService {
    public Map<String, String> getToken(String username, String password);
    }
  2. 实现service/impl/user/account/LoginServiceImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    package com.kob.backend.service.impl.user.account;

    import com.kob.backend.pojo.User;
    import com.kob.backend.service.impl.utils.UserDetailsImpl;
    import com.kob.backend.service.user.account.LoginService;
    import com.kob.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.security.PrivateKey;
    import java.util.HashMap;
    import java.util.Map;

    /**
    * @author xzt
    * @version 1.0
    */
    @Service
    public class LoginServiceImpl implements LoginService {
    @Autowired
    private AuthenticationManager authenticationManager; // 用来验证用户是否能登录成功

    /**
    * 实现用户的登录
    * @param username 用户名
    * @param password 密码
    * @return
    */
    @Override
    public Map<String, String> getToken(String username, String password) {
    // 将用户名密码进行封装,将密码加密
    UsernamePasswordAuthenticationToken authenticationToken =
    new UsernamePasswordAuthenticationToken(username, password);
    Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 登录失败,会自动处理

    // 登录成功,取出用户信息
    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_msg", "success");
    map.put("token", jwt);
    return map;
    }
    }
  3. 实现controller/user/account/LoginController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package com.kob.backend.controller.user.account;

    import com.kob.backend.service.user.account.LoginService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;

    import java.util.Map;

    /**
    * @author xzt
    * @version 1.0
    */
    @RestController
    @RequestMapping("/user/account")
    public class LoginController {
    @Autowired
    private LoginService loginService;

    @PostMapping("/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);
    }
    }

  4. 使用PostMan进行测试结果

    image-20230313165434475

获得用户信息功能

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

  1. 实现service/user/account/InfoService

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.kob.backend.service.user.account;

    import org.springframework.stereotype.Service;

    import java.util.Map;

    /**
    * @author xzt
    * @version 1.0
    */
    public interface InfoService {
    public Map<String, String> getInfo();
    }
  2. 实现Service/user/account/InfoServiceImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package com.kob.backend.service.impl.user.account;

    import com.kob.backend.pojo.User;
    import com.kob.backend.service.impl.utils.UserDetailsImpl;
    import com.kob.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;

    /**
    * @author xzt
    * @version 1.0
    */
    @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_msg", "success");
    map.put("id", user.getId().toString());
    map.put("username", user.getUsername());
    map.put("password", user.getPassword());
    map.put("photo", user.getPhoto());
    return map;
    }
    }
  3. 实现controller/user/account/InfoController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package com.kob.backend.controller.user.account;

    import com.kob.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.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.Map;

    /**
    * @author xzt
    * @version 1.0
    */
    @RestController
    @RequestMapping("/user/account")
    public class InfoController {

    @Autowired
    private InfoService infoService;

    @GetMapping("/info")
    public Map<String, String> getInfo(){
    return infoService.getInfo();
    }
    }
  4. 使用PostMan测试结果

    image-20230313171615265

注册功能

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

  1. 实现/service/user/account/RegisterService

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.kob.backend.service.user.account;

    import org.springframework.stereotype.Service;

    import java.util.Map;

    /**
    * @author xzt
    * @version 1.0
    */
    public interface RegisterService {
    public Map<String, String> register(String username, String password, String confirmPassword);
    }
  2. 实现/service/impl/user/account/RegisterServiceImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    package com.kob.backend.service.impl.user.account;

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

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

    /**
    * @author xzt
    * @version 1.0
    */
    @Service
    public class RegisterServiceImpl implements RegisterService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Map<String, String> register(String username, String password, String confirmPassword) {
    Map<String, String> map = new HashMap<>();
    if(username == null) {
    map.put("error_msg", "用户名不能为空");
    return map;
    }
    if(password == null || confirmPassword == null) {
    map.put("error_msg", "密码不能为空");
    return map;
    }
    username = username.trim();
    if(username.length() == 0) {
    map.put("error_msg", "用户名不能为空");
    return map;
    }
    if(password.length() == 0 || confirmPassword.length() == 0) {
    map.put("error_msg", "密码不能为空");
    return map;
    }
    if(username.length() > 100) {
    map.put("error_msg", "用户名长度不能大于100");
    return map;
    }
    if(password.length() > 100 || confirmPassword.length() > 100) {
    map.put("error_msg", "密码长度不能大于100");
    return map;
    }
    if(!password.equals(confirmPassword)){
    map.put("error_msg", "两次输入的密码不一致");
    return map;
    }

    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    List<User> users = userMapper.selectList(queryWrapper);
    if(!users.isEmpty()) {
    map.put("error_msg", "用户名已存在");
    return map;
    }
    String encodedPassword = passwordEncoder.encode(password); // 密码加密
    String photo = "https://cdn.acwing.com/media/user/profile/photo/72309_lg_e1afa7c633.jpg";
    User user = new User(null, username, encodedPassword, photo);

    userMapper.insert(user);
    map.put("error_msg", "success");
    return map;
    }
    }
  3. 实现controller/user/account/RegisterController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    package com.kob.backend.controller.user.account;

    import com.kob.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.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.Map;

    /**
    * @author xzt
    * @version 1.0
    */
    @RestController
    @RequestMapping("/user/account")
    public class RegisterController {

    @Autowired
    private RegisterService registerService;

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

  4. 使用PostMan进行测试

    image-20230313193446116

前端实现

页面创建

这里主要实现登录页面和注册页面的前端样式。

  1. 页面创建,需要在views/user/account/下创建两个页面UserAccountLoginView.vueUserAccountRegisterView.vue

  2. 将创建的页面加入路径中router.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 导入包
    import UserAccountLoginView from '@/views/user/account/UserAccountLoginView'
    import UserAccountRegisterView from '@/views/user/account/UserAccountRegisterView'

    // 在const routes = [] 中加入
    const routes = [
    {
    path: "/user/account/login/",
    name: "user_account_login",
    component: UserAccountLoginView,
    },
    {
    path: "/user/account/register/",
    name: "user_account_register",
    component: UserAccountRegisterView,
    },
    ]
  3. 实现一个Module用来保存登录的用户信息.store/user.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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: {
    }
    }
  4. 将实现的userModule加入全局Module中(store/index.js),需要导入ModuleUser包,然后加入到modules中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { createStore } from 'vuex'
    import ModulUser from '@/store/user'

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

登录页面实现

  1. 需要在user.js中实现一个登录函数login(),向后端提交一个请求并返回登录成功后用户的token;还需要实现getinfo()函数,用来将登录的用户信息保存在sotrestate中。这个函数需要写在actions中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    actions: {  // 实现函数,公有函数,可以被外面调用,然后调用私有函数对变量进行赋值
    login(context, data) { // 向后端发送请求,进行登录
    $.ajax({
    url: "http://localhost:3000/user/account/token/",
    type: "post",
    data: {
    username: data.username,
    password: data.password,
    },
    success(resp) {
    if(resp.error_msg === "success") {
    context.commit("updateToken", resp.token); // 调用mutations中的函数对store中的变量进行赋值
    data.success(resp);
    } else {
    data.error(resp);
    }
    },
    error(resp) {
    data.error(resp);
    }
    });
    },
    getinfo(context, data) { // 向后端发送请求,从用户的token来获取用户的详细信息。
    $.ajax({
    url: "http://localhost:3000/user/account/info/",
    type: "get",
    headers: {
    Authorization: "Bearer " + context.state.token,
    },
    success(resp) {
    if(resp.error_msg === "success") {
    context.commit("updateUser", {
    ...resp, // 将resp的内容解析出来
    is_login: true,
    });
    data.success(resp);
    } else {
    data.error(resp);
    }
    },
    error(resp) {
    data.error(resp);
    }
    });
    },
    }
  2. 实现登录页面UserAccountLoginView.vue,进行调用login函数,进行登录请求。如果登录成功后,再调用getinfo函数向后端发送请求,来获取用户的详细信息。如果获取成功,则跳转至主页面home页面。

    需要注意的是,这里调用storeactions中的函数的调用方法是store.dispatch("用户名", {参数, 回调函数})

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    <template>
    <ContentField>
    <div class="row justify-content-md-center">
    <div class="col-3">
    <form @submit.prevent="login">
    <div class="mb-3">
    <label for="username" class="form-label">用户名</label>
    <input v-model="username" type="text" class="form-control" id="username" aria-describedby="请输入用户名">
    </div>
    <div class="mb-3">
    <label for="password" class="form-label">密码</label>
    <input v-model="password" type="password" class="form-control" id="password" aria-describedby="请输入密码">
    </div>
    <div class = "error_msg">{{ error_msg }}</div>
    <button type="submit" class="btn btn-primary">登录</button>
    </form>
    </div>
    </div>
    </ContentField>
    </template>


    <script>
    import ContentField from '@/components/ContentField.vue'
    import { useStore } from 'vuex';
    import { ref } from 'vue';
    import router from '@/router/index';

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

    const login = () => {
    error_msg.value = ""; // 清空error_msg
    store.dispatch("login", { // 调用actions里的函数
    username: username.value,
    password: password.value,
    success() {
    // 登录成功,需要获取用户的详细信息
    store.dispatch("getinfo", {
    success() {
    router.push({name: 'home'});
    console.log(store.state.user);
    }
    });
    },
    error() {
    error_msg.value = "用户名或密码错误";
    }
    });
    }

    return {
    username,
    password,
    error_msg,
    login,
    }
    }
    }

    </script>

    <style scoped>
    button {
    width: 100%;
    }
    div.error_msg {
    color: red;
    }

    </style>

导航栏优化

需要实现,当用户没有登录时,导航栏右侧显示的是【登录】【注册】两个按钮,当用户登录成功后,显示用户的用户名和一个下拉栏。

实现步骤

  • 首先可以使用v-if="$store.state.user.is_login"v-else通过判断sotre.state.user中的is_login变量来判断是否登录。
  • 如果已登录显示前部分的ul,并需要使用{{ $store.state.user.username }}方法获取对应的用户名。
  • 如果没登录,则实现两个按钮,并通过:to实现当点击时跳转至对应的页面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<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>
<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>

实现效果

未登录的页面,右上角显示【登录】【注册】按钮

image-20230314110816230

点击登录按钮跳转至登录页面

image-20230314110849310

登录成功后跳转回主页面并在右上角显示用户信息

image-20230314110925711

实现退出功能

实现思想:将用户存在sotre.state.user中的token及用户信息删除即可。

实现步骤

  • /store/user.jsmutations中添加函数logout,用来将state中保存的信息清空,并将is_login设为false

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mutations: {  // 用来给state赋值,相当于set(), 但是是私有的,
    logout(state) { // 退出登录
    state.id = "";
    state.username = "";
    state.photo = "";
    state.token = "";
    state.is_login = false;
    },
    },
  • /store/user.jsactions中添加函数logout,用来调用mutations中的函数logout

    1
    2
    3
    4
    5
    actions: {  // 
    logout(context) {
    context.commit("logout");
    },
    }
  • 修改NavBar.vue,编写事件通过store.dispatch("logout")调用actions中的函数logout。并实现点击【退出】按钮时调用logout事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
     <li><a class="dropdown-item" href="#" @click="logout">退出</a></li>


    <script>
    import { useRoute } from 'vue-router'
    import { computed } from 'vue' // 进行实时计算
    import { useStore } from 'vuex';

    export default {
    setup() { // 入口
    const store = useStore();
    const route = useRoute(); // 取得当前是哪个页面
    let route_name = computed(() => route.name);

    const logout = () => { // 退出登录事件
    store.dispatch("logout");
    }
    return {
    route_name,
    logout,
    }
    }
    }

    </script>

实现效果

image-20230314111831562

点击退出后,需要重新登录

image-20230314111854482

实现前端页面的授权

这一部分可以使用后端编写过滤器进行实现,这里是从js实现的。

实现前端页面的授权,就是在当没有登录的时候,某些页面不能访问

实现步骤

1.在router/index.js中每个路径后面加个是否需要授权的字段。requestAuth 为true时,表示该页面需要授权,为false时表示该页面不需要授权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
const routes = [
{
path: "/",
name: "home",
redirect: "/pk/",
meta: {
requestAuth: true,
}
},
{
path: "/pk/",
name: "pk_index",
component: PkIndexView, // 地址栏输入 localhost:8080/pk/ 即可显示 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: "/404/",
name: "404",
component: NotFound,
meta: {
requestAuth: false,
}
},
{
path: "/user/bot/",
name: "user_bot_index",
component: UserBotIndexView,
meta: {
requestAuth: true,
}
},
{
path: "/user/account/login/",
name: "user_account_login",
component: UserAccountLoginView,
meta: {
requestAuth: false,
}
},
{
path: "/user/account/register/",
name: "user_account_register",
component: UserAccountRegisterView,
meta: {
requestAuth: false,
}
},
{
path: "/:catchAll(.*)", // 输入格式错误或乱码,则重定向至404页面
redirect: "/404/"
},
]

2.然后在router/index.js中实现router.beforeEach((to, from, next)函数,这个函数会在每次router之前执行,,每次通过router进入页面之前会调用该函数。有三个参数:

  • to: 跳转至哪个页面
  • from: 从哪个页面跳转过去
  • next: 页面执行下一步操作

具体实现思路就是当要跳转的页面需要授权并且此时没有登录时,则重定向到user_account_login页面

1
2
3
4
5
6
7
8
9
// router起作用之前执行,每次通过router进入页面之前会调用该函数,
// to: 跳转至那个页面 from:从哪个页面跳转过去 next: 页面要不要执行下一步操作
router.beforeEach((to, from, next) => {
if(to.meta.requestAuth && !store.state.user.is_login) { // 如果该页面是需要授权并且没有登录的时候,重定向到登录页面
next({name : "user_account_login"});
} else {
next();
}
})

实现效果

不管点击除登录注册之外的其他页面都会重定向到登录页面。

image-20230314150852465

注册页面实现

注册页面的实现和登录页面的实现大同小异。直接上代码:

需要注意的是:form表单上的@submit.prevent=需要改为下面定义的事件的名字register

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<template>
<ContentField>
<div class="row justify-content-md-center">
<div class="col-3">
<form @submit.prevent="register">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input v-model="username" type="text" class="form-control" id="username" aria-describedby="请输入用户名">
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input v-model="password" type="password" class="form-control" id="password" aria-describedby="请输入密码">
</div>
<div class="mb-3">
<label for="confirmedPassword" class="form-label">确认密码</label>
<input v-model="confirmedPassword" type="password" class="form-control" id="confirmedPassword" aria-describedby="请再次输入密码">
</div>
<div class = "error_msg">{{ error_msg }}</div>
<button type="submit" class="btn btn-primary">注册</button>
</form>
</div>
</div>
</ContentField>
</template>


<script>
import ContentField from '@/components/ContentField.vue'
import { ref } from 'vue'; // export 则需要加{}
import router from '@/router'; // export default 则不需要加{}
import $ from 'jquery'

export default {
components: {
ContentField
},

setup() {
let username = ref("");
let password = ref("");
let confirmedPassword = ref("");
let error_msg = ref("");

const register = () => {
$.ajax({
url: "http://localhost:3000/user/account/register/",
type: "post",
data: {
username: username.value,
password: password.value,
confirmedPassword: confirmedPassword.value,
},
success(resp) {
if(resp.error_msg === "success") {
router.push({name : "user_account_login"});
} else {
error_msg.value = resp.error_msg;
}
},
});
}

return {
username,
password,
confirmedPassword,
error_msg,
register,
}
}

}
</script>

<style scoped>
button {
width: 100%;
}
div.error_msg {
color: red;
}
</style>

实现效果

image-20230314153948955

登录状态持久化

之前只是吧用户登录后的token保存在了jsstore.state中,所以每次刷新页面,token都会消失,然后需要重新登录。这样显然是不符合业务的。因此需要将token保存在浏览器的一小块硬盘空间中localStorage

实现步骤

1.当登录成功后将token保存在localStorage中,localStorage.setItem("jwt_token", resp.token);。在store/user.js中修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
login(context, data) {  // 登录
$.ajax({
url: "http://localhost:3000/user/account/token/",
type: "post",
data: {
username: data.username,
password: data.password,
},
success(resp) {
if(resp.error_msg === "success") {
localStorage.setItem("jwt_token", resp.token);
context.commit("updateToken", resp.token);
data.success(resp);
} else {
data.error(resp);
}
},
error(resp) {
data.error(resp);
}
});
},

2.当退出时,需要将localStorage保存的token删除.在store/user.js中修改。

1
2
3
4
logout(context) {
localStorage.removeItem("jwt_token");
context.commit("logout");
}

3.在所有的加载登录页面前判断是否有jwt_token,如果有,则直接更新sotre.state.user.token并从后端获取用户信息。在views/user/account/UserAccountLoginView.vue中添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
setup() {

const jwt_token = localStorage.getItem("jwt_token"); // 将浏览器中的jwt_token取出来
if(jwt_token) { // 如果浏览器中有token
store.commit("updateToken", jwt_token); // 调用user.js的updateToken函数
store.dispatch("getinfo", {
success() {
router.push({ name : "home" });
},
error() {

},
})
} else {

}

}
</script>

页面优化

如果已经登录,还是会先跳转至登录页面,再跳转至主页面,虽然很快,但是会闪一下。同样的,右上角的用户名和登录注册之间也会闪一下,为了避免这种问题,需要对页面进行优化。

1.首先,先在store/user.js中的state添加一个变量pulling_info,如果为true则表示正在从后端拉去信息。换句话说,当为false的时候则需要显示登录页面和登录注册按钮。

2.然后,再在store/user.jsmutation中添加更新pulling_info的函数

1
2
3
updatePullingInfo(state, pulling_info) {
state.pulling_info = pulling_info;
},

3.修改views/user/account/UserAccountLoginView.vue,在组件上加上v-if="!$store.state.user.pulling_info",如果pulling_info是false就显示登录页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<ContentField v-if="!$store.state.user.pulling_info">
....
</ContentField>

<script>
// 修改
const jwt_token = localStorage.getItem("jwt_token"); // 将浏览器中的jwt_token取出来
if(jwt_token) { // 如果浏览器中有token
store.commit("updateToken", jwt_token); // 调用user.js的updateToken函数
store.dispatch("getinfo", {
success() {
router.push({ name : "home" });
store.commit("updatePullingInfo", true);
},
error() {
store.commit("updatePullingInfo", false);
},
})
} else {
store.commit("updatePullingInfo", false);
}
</script>

4.修改components/NavBar.vue,在登录按钮组件中加上v-if="!$store.state.user.pulling_info"

1
2
3
4
5
6
7
8
9
10
11
12
<ul class="navbar-nav" v-else-if="!$store.state.user.pulling_info">
<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>

实现效果

可以看到,在浏览器中的localStorage中已经保存了jwt_token

image-20230314154722620

当登录成功后,不管怎么刷新都不会自动退出登录了。

image-20230314163306133

正在加载今日诗词....