企业级 SpingBoot3 实现登录

所用的技术

  • springboot
  • jwt
  • maven
  • mysql
  • mybatis
  • knife4j
  • springdata jpa

准备工作

maven依赖

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
       <!--mysql依赖-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--mybatis依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>3.0.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>4.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>5.3.22</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-core</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-spring-web</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.5.20</version>
        </dependency>
        <dependency>
            <groupId>jakarta.persistence</groupId>
            <artifactId>jakarta.persistence-api</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>3.3.2</version>
        </dependency>

数据库

CREATE DATABASE IF NOT EXISTS database_name  

CREATE TABLE `user` (
  `id` bigint(11) NOT NULL COMMENT '主键',
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  `username` varchar(255) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `status` int(11) DEFAULT NULL COMMENT '0:禁用  1:启用',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `create_by` int(11) DEFAULT NULL COMMENT '创建人id',
  `update_by` int(11) DEFAULT NULL COMMENT '修改人id',
  `del_flag` int(11) DEFAULT NULL COMMENT '0:删除 1:未删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息';

yml配置

application.yml

server:
  port: 8080

spring:
  profiles:
    active: dev
  main:
    allow-circular-references: true
  datasource:
    druid:
      driver-class-name: ${sky.datasource.driver-class-name}
      url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: ${sky.datasource.username}
      password: ${sky.datasource.password}
  redis:
    host: ${sky.redis.host}
    port: ${sky.redis.port}
    password: ${sky.redis.password}
    database: ${sky.redis.database}

mybatis:
  #mapper配置文件
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.sky.entity
  configuration:
    #开启驼峰命名
    map-underscore-to-camel-case: true

logging:
  level:
    com:
      sky:
        mapper: debug
        service: info
        controller: info

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    user-secret-key: ithema
    user-ttl: 7200000
    user-token-name: authentication
  alioss:
    endpoint: ${sky.alioss.endpoint}
    access-key-id: ${sky.alioss.access-key-id}
    access-key-secret: ${sky.alioss.access-key-secret}
    bucket-name: ${sky.alioss.bucket-name}
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}
    mchid: ${sky.wechat.mchid}
    mchSerialNo: ${sky.wechat.mchSerialNo}
    privateKeyFilePath: ${sky.wechat.privateKeyFilePath}
    apiV3Key: ${sky.wechat.apiV3Key}
    weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath}
    notifyUrl: ${sky.wechat.notifyUrl}
    refundNotifyUrl: ${sky.wechat.refundNotifyUrl}

 application-dev.yml

wzy:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: database_name
    username: 账号
    password: 密码

实体类

entity


@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user")
public class User {

    // 用户唯一标识
    @Id
    private Long id;

    // 姓名
    private String name;

    // 用户名
    private String username;

    // 密码
    private String password;

    // 状态: 0:禁用 1:启用
    private Integer status;

    // 创建时间
    private LocalDateTime createTime;

    // 更新时间
    private LocalDateTime updateTime;

    // 创建人
    private Integer createBy;

    // 更新人
    private Integer updateBy;

    // 删除标识: 0:未删除 1:已删除
    private Integer delFlag;

}

dto

后端接受前端传来的信息

@Data
@ApiModel(description = "用户登录信息")
public class UserLoginDTO implements Serializable {

    @ApiModelProperty(value = "用户名", required = true)
    private String username;

    @ApiModelProperty(value = "密码", required = true)
    private String password;
}

vo

后端传给前端的信息

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "用户登录返回信息")
public class UserLoginVO implements Serializable {

    @ApiModelProperty("主键值")
    private Long id;

    @ApiModelProperty("jwt令牌")
    private String token;

    @ApiModelProperty("姓名")
    private String name;

    @ApiModelProperty("用户名")
    private String username;

}

result 数据返回类

作一个统一的返回值

@Data
public class Result<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据

    public static <T> Result<T> success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }

    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<T>();
        result.data = object;
        result.code = 1;
        return result;
    }

    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}

业务异常处理

给出现所有不合法的操作一个统一的处理,这里只列举的一部分,如果给还需其他异常处理,请自行添加

public class BaseException extends RuntimeException {
    public BaseException() {

    }

    public BaseException(String message) {
        super(message);
    }
}

密码错误异常处理

public class PasswordErrorException extends BaseException {
    public PasswordErrorException() {}

    public PasswordErrorException(String message) {
        super(message);
    }
}

用户不存在异常处理

public class UserNotFoundException extends BaseException{
    public UserNotFoundException() {}

    public UserNotFoundException(String message)
    {
        super(message);
    }
}

用户被禁用异常

public class UserStopException extends BaseException{
    public UserStopException() {}
    public UserStopException(String message) {
        super(message);
    }
}

全局异常处理

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 捕获全局异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex) {
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }
}

消息提示类常量

将其作为类封装,便于后期的维护

public class MessageConstant {
    public static final String USER_NOT_FOUND = "用户不存在";
    public static final String USER_NOT_ENABLE = "用户未启用";
    public static final String USER_NOT_LOGIN = "用户未登录";
    public static final String UNKNOWN_ERROR = "未知错误";
    public static final String USER_LOGIN_SUCCESS = "用户登录成功";
    public static final String LOGIN_FAILED = "登录失败";
    public static final String PASSWORD_ERROR = "密码错误";
}

Knife4j配置

kniife4j:目前主流的接口显示文档

@Configuration
public class Knife4jConfig {//对于配置类要求可以看懂即可,不用反复去写,将来可以CV
    @Bean
    public OpenAPI springShopOpenApi() {
        return new OpenAPI()
                // 接口文档标题
                .info(new Info().title("demo")
                        // 接口文档简介
                        .description("demo_demo")
                        // 接口文档版本
                        .version("1.0.0")
                        // 开发者联系方式
                        .contact(new Contact().name("顾随")
                                .email("****")));

    }
}

使用threadLocal存储用户id

使用线程池存储用户id,便于后期数据使用

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

jwt

原理

  • 前端发请求,将有账号和密码发给后端
  • 后端核对前端发来的账号和密码,如果正确,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串。如果错误,则会之间报异常,封装统一result后,返回给前端。
  • 后端将一个用户数据和生成的token,返回给前端
  • 前端每次请求,都要将后端传来的token放入http请求的Authorization属性中
  • 后端接受前端请求时,先检查前端传来的token,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
  • 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

生成jwt相关配置

JwtProperties接受xml配置中的参数,放入其属性中

@Component
@ConfigurationProperties(prefix = "wzy.jwt")
@Data
public class JwtProperties {

    private String userSecretKey;
    private long userTtl;
    private String userTokenName;
}

jwt工具类

用于生成jwt和解析jwt

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);
        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

jwt检验拦截器

用户校验前端传来的token

@RequiredArgsConstructor
@Slf4j
@Component
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    private final JwtProperties jwtProperties;

    /**
     * 拦截请求进行jwt令牌认证
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            // 不是Controller的方法直接通过
            return true;
        }

        // 从请求头中获取token
        String token = request.getHeader(jwtProperties.getUserTokenName());

        // 校验token
        try {
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(),token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            BaseContext.setCurrentId(userId);
            // 通过,放行
            return true;
        } catch (Exception e) {
            // 不通过,响应401状态码
            response.setStatus(401);
            return false;
        }

    }

}

业务编写

controller

@RestController
@RequestMapping("/user")
@Slf4j
@Tag(name = "用户登录接口")
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;

    private final JwtProperties jwtProperties;
    @Operation(summary = "用户登录")
    @PostMapping("/login")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDto)
    {
        log.info("用户登录:{}",userLoginDto);
        User user = loginService.login(userLoginDto);
        log.info("登录用户:{}",user);

        // 登录成功后生成token
        Map<String,Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID,user.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getUserSecretKey(),
                jwtProperties.getUserTtl(),
                claims
        );

        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .name(user.getName())
                .token(token)
                .username(user.getUsername())
                .build();

        return Result.success(userLoginVO);
    }
}

service

public interface LoginService {

    /**
     * 用户登录
     * @param userLoginDto
     * @return
     */
    User login(UserLoginDTO userLoginDto);
}

serviceImpl

@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService {

//    private final UserRepository userRepository;

    private final UserMapper userMapper;

    /**
     * 用户登录
     * @param userLoginDto
     * @return
     */
    @Override
    public User login(UserLoginDTO userLoginDto) {
        String username = userLoginDto.getUsername();
        String password = userLoginDto.getPassword();

        User user = userMapper.findByUsername(username, DelFlagConstant.ENABLE);

        // 处理各种异常
        if (user == null) {
            // 用户不存在
            throw new UserNotFoundException(MessageConstant.USER_NOT_FOUND);
        }

        //密码比对
        //对前端传来的明文密码进行加密
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        if (!password.equals(user.getPassword())) {
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }

        if (user.getStatus() == StatusConstant.STOP) {
            throw new UserStopException(MessageConstant.USER_NOT_ENABLE);
        }

        return user;
    }
}

Mapper

@Mapper
public interface UserMapper {

    @Select("select * from user where username = #{username} and del_flag = #{delFlag}")
    User findByUsername(String username, Integer delFlag);
}

运行

打开http://localhost:8090/doc.html,查看knife4j生成的接口文档

出现图中内容,表示knife4j部署成功

测试接口

登录成功

输入正确的账号,密码后,会出现token以及用户信息

密码错误情况

返回值中会显示“密码错误”

用户账号错误情况

返回值会出现:“用户不存在”

出现以上情况,表示后端登录配置成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值