一般的登录权限框架有spring security和shiro两种,但是如果只是单单实现一个登录功能,没有更多的权限交互,引入沉重的框架来处理反而会出现很多问题或者加重程序的处理效率,所以,用自定义的token来实现登录功能在一些小开发中也可以使用到.
首先推荐大家一款非常实用的工具包—hutool,这个工具包整合了很多细节方法,平时开发中利用这个包可以实现很多之前觉得非常复杂的业务,在本案例中,我也采用了hutool中的一些方法,着实舒服!!
先介绍一下我用到的hutool中的方法吧!!
对于登录功能,当然最主要的还是密码的加密校验和token的生成,在这里我使用相对比较严谨的hutool中的签名和验证进行,当然,如果业务过于简单,也可以使用UUID来充当token(顺便说一句,hutool中也有生成UUID的方法,而且没有"-"哦!!)
话不多说,开整 !
1.首先,引入相关的maven包
<!--redis:用户存放token信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<!--hutool:工具类,用于密码加密,生成token -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.1</version>
</dependency>
<!--前后端交互-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql连接-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--简化实体类-->
<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>
<!--mybatis-plus:mybatis孪生兄弟,用过的都说好-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
至于配置文件,百度一下mysql的连接配置和redis的连接配置即可
2.创建数据库表和实体类
新建admin表:
创建实体类:
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("admin")
public class Admin implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String password;
}
2.requestDTO 用于以json格式接收前端数据
import lombok.Data;
@Data
//登录传输数据
public class LoginDTO {
private String username;
private String password;
}
import lombok.Data;
@Data
//新增管理员传输数据
public class ReqAddAdmin {
private String username;
private String password;
}
3.编写登录方法(需要注意的是,在第一次编写的时候千万不要加密密码,否则无法校验成功,所以我在这里新建了一个添加管理员方法,明文登录后添加一个加密后的管理员账号,然后就可以把之前的管理员删除了)
或者还有一种简单的方法,用bcript直接将密码加密后打印到控制台,然后复制到数据库中,bcript方法在hutool工具包中有DigestUtil.bcrypt(password),在代码中会有展示.
1.controller
package com.ccas.admin.controller;
import com.ccas.admin.common.Result;
import com.ccas.admin.common.StatusCode;
import com.ccas.admin.request.LoginDTO;
import com.ccas.admin.request.ReqAddAdmin;
import com.ccas.admin.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("admin")
public class AdminController {
@Autowired
private AdminService adminService;
@PostMapping("login")
public Result<Object> login(@RequestBody LoginDTO loginDTO, HttpServletResponse response) {
//service层会处理具体逻辑,处理完成后会返回一个token,如果token为null,则返回错误信息,如果token不为null,则登录成功,且将token返回给前端
String token = adminService.findByUsername(loginDTO,response);
if (token == null) {
return new Result<>(StatusCode.UNAUTHORIZED, "账号或密码错误");
}
return new Result<>(StatusCode.SUCCESS, "登录成功", token);
}
@PostMapping("addAdmin")
public Result<Object> addAdmin(HttpServletRequest request, @RequestBody ReqAddAdmin reqAddAdmin) {
//service层会校验token的有效性,如果token无效,则返回错误信息,如果token有效,则处理具体业务逻辑,这里是添加一个管理员
if (!adminService.checkToken(request)) {
return new Result<>(StatusCode.UNAUTHORIZED, "添加失败");
}
adminService.addAdmin(reqAddAdmin);
return new Result<>(StatusCode.SUCCESS, "添加成功");
}
}
2.service
package com.ccas.admin.service;
import com.ccas.admin.request.LoginDTO;
import com.ccas.admin.request.ReqAddAdmin;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface AdminService {
String findByUsername(LoginDTO loginDTO, HttpServletResponse response);
boolean checkToken(HttpServletRequest request);
void addAdmin(ReqAddAdmin reqAddAdmin);
}
3.serviveImpl
package com.ccas.admin.service.impl;
import cn.hutool.core.codec.Base64Decoder;
import cn.hutool.core.codec.Base64Encoder;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.Sign;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ccas.admin.dao.AdminMapper;
import com.ccas.admin.domain.Admin;
import com.ccas.admin.request.LoginDTO;
import com.ccas.admin.request.ReqAddAdmin;
import com.ccas.admin.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
@Service
@Transactional
public class AdminServiceImpl implements AdminService {
@Autowired
private AdminMapper adminMapper;
@Autowired
private StringRedisTemplate redisTemplate;
//全局定义一个唯一的sign对象,这是用户签名和验证的对象.
//之前尝试过在方法内部建立该对象,由于签名和验证不在同一个方法中,两次新建该对象会产生不同的签名对象,会导致签名和验证不匹配,校验不通过
private final Sign sign = SecureUtil.sign(SignAlgorithm.MD5withRSA);
@Override
public String findByUsername(LoginDTO loginDTO, HttpServletResponse response) {
String token = null;
//用mybatis-plus查询,根据用户名查询对象,如果不存在该对象,则直接返回token=null
Admin admin = adminMapper.selectOne(new QueryWrapper<Admin>().eq("username", loginDTO.getUsername()));
if (admin == null) {
return null;
}
//如果存在该对象,则校验密码,密码是用bcript加密的,可以用hutool中的工具类来校验,当然后面的密码加密也使用hutool的bcript加密
//如果密码不正确,则返回token=null
if (!DigestUtil.bcryptCheck(loginDTO.getPassword(), admin.getPassword())) {
return null;
}
//接下来就是签名了,这一步比较难理解,可以去hutool官网的签名例子,非常好理解
//1.首先我们将签名数据转换为字节数组,因为这是他方法参数需要传递的数据类型
byte[] data = admin.getUsername().getBytes();
//2.然后我们将这字节数组放到sign对象的sign方法中,注意,sign对象是全局唯一的,然后就会生成一个字节数组的签名signed
byte[] signed = sign.sign(data);
//为了能够保证签名的时效性,我们需要将这个签名放到Redis中,并设置过期时间
//然而,这是一个字节数组,存入redis非常困难,于是我们可以先签名通过base64进行转码,解析为字符串形式
token = Base64Encoder.encode(signed);
//最后,我们至于要将token放在redis中,这里需要注意的是,我们可以将token设置为key,用户id设置为value
//这样的好处是:我们可以直接判断是否存在key,并且可以通过key获取到管理员信息
redisTemplate.boundValueOps(token).set(admin.getId().toString(), 1, TimeUnit.DAYS);
return token;
}
@Override
public boolean checkToken(HttpServletRequest request) {
//前端会将token放置在请求头中,我们就将请求头中将token拿出来
String token = request.getHeader("token");
//只有在请求头中拿到token,并且该token存在于redis中,才进行后续操作,否则直接返回null
if (token != null && redisTemplate.hasKey(token)) {
//从redis中获取到管理员id
Integer adminId = Integer.valueOf(redisTemplate.boundValueOps(token).get());
//通过id查询到admin对象
Admin admin = adminMapper.selectById(adminId);
//获取对象用户名
String username = admin.getUsername();
//签名校验,该方法需要传递两个参数,一个是签名数据(即上个方法中的data),一个是签名(即上个方法中的signed)
//签名数据其实对应的就是用户名username,签名其实对应的就是token,但他们都是字符串,所以我们需要将其转化为字节数组
//签名数据可以直接用getBytes()方法,但是token由于是base64解析的,所以我们需要通过base64反解析回字节数组,而不能用普通的getBytes()方法
return sign.verify(username.getBytes(), Base64Decoder.decode(token));
}
return false;
}
@Override
public void addAdmin(ReqAddAdmin reqAddAdmin) {
Admin admin = new Admin();
admin.setUsername(reqAddAdmin.getUsername());
//通过bcript加密
admin.setPassword(DigestUtil.bcrypt(reqAddAdmin.getPassword()));
//插入数据库
adminMapper.insert(admin);
}
}
4.dao
package com.ccas.admin.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ccas.admin.domain.Admin;
import org.springframework.stereotype.Repository;
@Repository
public interface AdminMapper extends BaseMapper<Admin> {
}
总结:大概的实现思路是
1.前端传入账号密码登录,后端进行校验(注意密码的加密)
2.如果校验通过,则通过签名生成token,登录成功并将token返回给前端
3.前端将token放在头信息中,调用其他方法时,后端会尝试去获取头信息中是否含有token,并获取该token
4.签名校验,如果token信息合法,则处理业务,如果不合法,则返回统一状态码,前端可根据该状态码提示对应信息或跳转到登录界面