做一个springboot登陆注册功能

目录

一、环境搭建

1、数据库

2、引入依赖 

 3、配置信息

4、创建包结构和数据库实体类

二、接口开发-注册接口

前提准备

响应数据

需求分析

全局异常处理

代码编写

 测试

三、接口开发-登录接口

前提准备

响应数据

需求分析

代码编写

测试

拦截器 

测试


一、环境搭建

1、数据库

-- 创建数据库
create database big_event;

-- 使用数据库
use big_event;

-- 用户表
create table user (
                      id int unsigned primary key auto_increment comment 'ID',
                      username varchar(20) not null unique comment '用户名',
                      password varchar(32)  comment '密码',
                      nickname varchar(10)  default '' comment '昵称',
                      email varchar(128) default '' comment '邮箱',
                      user_pic varchar(128) default '' comment '头像',
                      create_time datetime not null comment '创建时间',
                      update_time datetime not null comment '修改时间'
) comment '用户表';

-- 分类表
create table category(
                         id int unsigned primary key auto_increment comment 'ID',
                         category_name varchar(32) not null comment '分类名称',
                         category_alias varchar(32) not null comment '分类别名',
                         create_user int unsigned not null comment '创建人ID',
                         create_time datetime not null comment '创建时间',
                         update_time datetime not null comment '修改时间',
                         constraint fk_category_user foreign key (create_user) references user(id) -- 外键约束
);

-- 文章表
create table article(
                        id int unsigned primary key auto_increment comment 'ID',
                        title varchar(30) not null comment '文章标题',
                        content varchar(10000) not null comment '文章内容',
                        cover_img varchar(128) not null  comment '文章封面',
                        state varchar(3) default '草稿' comment '文章状态: 只能是[已发布] 或者 [草稿]',
                        category_id int unsigned comment '文章分类ID',
                        create_user int unsigned not null comment '创建人ID',
                        create_time datetime not null comment '创建时间',
                        update_time datetime not null comment '修改时间',
                        constraint fk_article_category foreign key (category_id) references category(id),-- 外键约束
                        constraint fk_article_user foreign key (create_user) references user(id) -- 外键约束
)

2、引入依赖 

        <dependency>    
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.2</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

 3、配置信息

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Drive
    url: jdbc:mysql://localhost:3306/big_event
    username: root
    password: 123456
    
server:
  port: 8888

4、创建包结构和数据库实体类


二、接口开发-注册接口

前提准备

先看user实体类,我们之所以没有为字段设置getter和setter就是因为我们可以为了代码的美观与简洁而使用lombok工具

lombok:在编译阶段,为实体类自动生成getter、setter、toString

使用方法就是再类上加上注解@Data

响应数据

在实际的项目开发中,不会像之前写的代码一样,直接return回去一个字符串或者数据。而是有统一的响应结果的,也就是Result类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;//业务状态码  0-成功  1-失败
    private String message;//提示信息
    private T data;//响应数据

    //快速返回操作成功响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    //快速返回操作成功响应结果
    public static Result success() {
        return new Result(0, "操作成功", null);
    }

    public static Result error(String message) {
        return new Result(1, message, null);
    }
}

@NoArgsConstructor:自动创建无参构造器
@AllArgsConstructor:自动创建有参构造器

需求分析

        注册功能,首先就要去确认用户名是否被占用,之后才能注册。其次既然是提交注册表单请求,那么一般就属于POST请求,而查询等业务才是GET请求。

        既然要注册用户,那么就不能原封不动的将密码加入数据库,一定要对数据进行加密,这样才安全。我们这里使用MD5加密,可以直接使用引入工具类也可以选择引入依赖。这里就使用MD5工具类了。

        除次之外还要对参数进行校验,比如要求是密码要是5~16位非空字符,那么就要先检查是否符合要求,其次才能添加进数据库。

Spring提供了一个参数校验框架,使用预定义的注解完成参数校验,叫做Spring Validation。

MD5加密与SpringValidation框架的使用方法icon-default.png?t=N7T8https://blog.csdn.net/m0_56308072/article/details/131101062?spm=1001.2014.3001.5501

全局异常处理

这个的目的就是因为当我们测试失败时,响应得到的接过并不符合result风格,也不美观。

我们更希望即使报这样的错误也要符合result类,因此我们可以定义一个全局类

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e){
        e.printStackTrace();
        //有的异常可能没有message,所以要先用三元运算符判断一下
        return Result.error(StringUtils
                .hasLength(e.getMessage()) ? e.getMessage() : "操作失败");
    }

}

这样就可以捕获全局的异常了。

代码编写

Controller层

@Validated
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")   //校验
    public Result register(@Pattern(regexp = "^\\S{5,16}$") String username,
                           @Pattern(regexp = "^\\S{5,16}$") String password) {

        //查询用户
        User user = userService.findByUserName(username);
        if (user == null){
            //注册
            userService.register(username,password);
            return Result.success();
        }else {
            //占用
            return Result.error("用户名以及被占用");
        }

    }
}

 Service层

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;
    @Override
    public User findByUserName(String name) {
        return userMapper.findByUserName(name);
    }

    @Override
    public void register(String username, String password) {
        //加密
        String md5Password = Md5Util.getMD5String(password);
        //添加
        userMapper.register(username,md5Password);
    }
}

 Mapper层

@Mapper
public interface UserMapper {

    @Select("select * from user where username = #{name}")
    User findByUserName(String name);

    @Insert("insert into user(username,password,create_time,update_time)" +
            "values(#{username},#{md5Password},now(),now())")
    void register(String username, String md5Password);
}

 测试


三、接口开发-登录接口

前提准备

        既然是开发登录接口,那么最多的就是使用JWT令牌进行登录

什么是JWT?如何使用icon-default.png?t=N7T8https://blog.csdn.net/m0_56308072/article/details/131144785?spm=1001.2014.3001.5501

导入依赖

        <!-- JWT -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.4.0</version>
        </dependency>

响应数据

需求分析

为什么要使用jwt令牌,简单来说就是因为,我们其实是可以直接跳过登录验证/login,直接进入/list去访问数据。这当然是不符合安全的。

因此才需要一块jwt令牌,只有通过/login才能可以拿到令牌,然后访问/list的时候只有持有令牌才可以进行访问

令牌就是一段字符串,作用是承载业务数据,减少后续请求查询数据库的次数。

防止篡改,保证信息的合法性和有效性

        用户登陆后,系统会自动下发JWT令牌,然后在后续的请求中,浏览器都需要在请求头header中携带到服务端,请求头的名称为Authorization,值为登陆时下发的JWT令牌。就是jwt携带在请求头中,我们在获取验证的时候就要hetHeader得到token然后解析验证。

        如果检测到用户未登录,则http响应状态码为401。那么就需要一个responese对象(HttpServletResponse),它可以更改状态码

首先就要根据输入的用户名进行查询,判断用户是否存在,然后再查询密码进行登录验证

代码编写

JwtUtil

public class JwtUtil {

    private static final String KEY = "wal";

    //接收业务数据,生成token并返回
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
                .sign(Algorithm.HMAC256(KEY));
    }

    //接收token,验证token,并返回业务数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))
                .build()
                .verify(token)
                .getClaim("claims")
                .asMap();
    }

}

Controller层

登录成功,生成token

    @PostMapping("/login")
    public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username,
                                @Pattern(regexp = "^\\S{5,16}$") String password){
        //查询用户
        User loginUser = userService.findByUserName(username);

        //判断用户是否存在
        if (loginUser == null){
            return Result.error("用户名错误");
        }

        //判断密码是否正确 loginUser对象中password是密文
        if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
            //登录成功,生成Token
            Map<String,Object> claims = new HashMap<>();
            claims.put("id",loginUser.getId());
            claims.put("username",loginUser.getUsername());
            String token = JwtUtil.genToken(claims);
            return Result.success(token);
        }

        return Result.error("密码错误");
    }

 访问其他接口,验证token

@RestController
@RequestMapping("/article")
public class ArticleController {
    @GetMapping("/list")
    public Result<String> list(@RequestHeader(name = "Authorization") String token,
                               HttpServletResponse response){
        //验证token
        try {
            Map<String, Object> claim = JwtUtil.parseToken(token);

            return Result.success("访问成功");
        } catch (Exception e) {
            //未登录
            response.setStatus(401);
            throw new RuntimeException(e);
        }

    }
}

测试

尝试跳过登录验证直接访问数据接口

正常登录,生成密钥

携带密钥访问数据接口

注:如果觉得每次都复制粘贴返回的token用来测试太麻烦,可以直接在这里设置一个全局的token,这样就不用每次测试接口的时候都在header里面粘贴上去token了。当然,改完记得save一下tab,不然不会生效。

但是注意,这里配置完全局token之后就不要再单独携带token,这样就会携带两个token过去,并报错

com.auth0.jwt.exceptions.JWTDecodeException: The token was expected to have 3 parts, but got > 3.  

拦截器 

向上面的例子中,我们只有一个数据访问接口的时候体现不出来,但是假如我们现在已经写了很多个接口,如果每个接口中都写验证token的逻辑未免也太繁琐了。我们希望这种复用性强的代码只写一次就好了,然后我们可以再不惊动原代码的情况下对功能进行添加,也就是AOP的思想。在这里应用的技术就是拦截器。

使用拦截器,首先要创建一个拦截器类然后集成HandlerInterceptor接口。其中重要的是preHandle方法

        preHandle方法:在目标方法执行前执行,也就是在你访问接口的时候就直接拦截下来。那么拦截下来之后就要验证token了。

我们之前是通过参数声明的方式直接拿到,但是在重写的这个方法中并没有。这是因为他直接被包含在了参数request中(HttpServletRequest),这个对象顾名思义,就是代表请求,所有的请求数据都在request对象中;反之,与其相对的是参数response(HttpServletRequest)代表响应,所有的响应数据都在response数据中。

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取令牌
        String token = request.getHeader("Authorization");
        //验证token
        try {
            Map<String,Object> claims = JwtUtil.parseToken(token);
            //没有异常就放行
            return true;
        } catch (Exception e) {
            //未登录,不放行
            response.setStatus(401);
            return false;
        }

    }
}

         此时虽然完成了拦截器,但是他还没有生效,需要先把他注册之后才会生效,保证安全。在拦截器上加上@Component注解那么就代表交给了IOC容器管理变成了一个bean对象。

        那么我们就在config创建一个拦截器的config表示启用哪些拦截器,重写其中的addIntercepter方法。顾名思义,就是添加那些拦截器。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //放行登录接口和注册接口
        registry.addInterceptor(loginInterceptor)
                .excludePathPatterns("/user/login","/user/register");
    }
}

注意:既然是拦截器,那么它就能拦截所有的接口。一定要注意不能让它拦截登录接口和注册接口,不然要访问数据接口就要登录生成的token,要登陆的token就要访问登录接口,由于登录接口也被拦截了,那么访问登录接口就要登录生成的token,要登录的token就要访问登录接口......开启了无限套娃。addInterceptor就是添加要使用的拦截器,excludePathPatterns就是要排除拦截的接口.

测试

当我们不携带令牌时,无响应,且状态码为401

携带令牌时成功访问 

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

An1ong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值