本系列文章着重讲的是前后端分离的后端部分接口的开发。对于大部分接口来说,我们不希望里面的数据被未登录的访客调用;希望建立服务器与用户间的”对话“。众所周知,http协议是无状态的,那么如何实现无状态连接的鉴权呢?

使用 Token 识别用户。

  • 客户端使用用户名跟密码请求登录

  • 服务端收到请求,去验证用户名与密码 

  • 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端 

  • 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里 

  • 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token

  • 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据 


1.客户端使用用户名跟密码请求登录


2.​服务端收到请求,去验证用户名与密码

账号密码登陆控制层

@PostMapping("/login")
@ApiOperation(value = "登录")
public ApiRes<Login> login(@RequestBody @Valid AccountLogin loginDto) {
    return accountService.login(loginDto);
}

3.验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端

服务层:先判断用户账号密码,再生成 Token 并保存 Redis,最后将结果返回

@Override
public ApiRes<Login> login(AccountLogin loginDto) {
    Employee emp = employeeMapper.selectOne(
            new QueryWrapper<Employee>().lambda()
                    .eq(Employee::getNo, loginDto.getAccount())
    );
    if (emp == null) {
        return ApiRes.fail("帐号不存在");
    }
    if (!emp.getPassword().equals(EncryptUtil.EncryptString(loginDto.getPassword()))) {
        return ApiRes.fail("帐号密码不正确");
    }
    if (emp.getRoleId() == null || emp.getEmployeeType() != EmployeeTypeEnum.支持员工.getValue()) {
        return ApiRes.fail("非支持员工,禁止登陆");
    }

    String token = UUID.randomUUID().toString().replaceAll("-", "");
    // Redis : DB.Sys -> Id:No:RoleId
    redisUtil.set(token, RedisPrefixKeyEnum.Sys.toString(), emp.getId() + ":" + emp.getNo() + ":" + emp.getRoleId(), expireTime);

    Role role = roleMapper.selectById(emp.getRoleId());

    Login loginData = new Login().setToken(token)
            .setId(emp.getId())
            .setAccount(emp.getNo())
            .setRealName(emp.getRealName())
            .setPhone(emp.getPhone())
            .setHeadImg(emp.getHeadPortrait())
            .setRoleId(emp.getRoleId())
            .setRoleName(role.getName())
            .setRoleDescription(role.getDescription())
            .setFailureTime(DateUtil.dateAddMinutes(null, expireTime));

    return ApiRes.suc("登录成功", loginData);
}

4.客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里 

​例(Vue.js):


保存在LocalStorage中:


5.客户端每次向服务端请求资源的时候需要带着服务端签发的 Token

前端处理(Vue.js + Axios):


6.服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

在spring boot 中添加 Filter 拦截器:

@Slf4j
public class TokenAuthFilter implements Filter {
    @Override
    public void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) var1;
        HttpServletResponse response = (HttpServletResponse) var2;

        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type,token");
        response.setHeader("Access-Control-Expose-Headers", "*");

        String methods = request.getMethod();
        if ("OPTIONS".equals(methods)) {
            response.setStatus(HttpServletResponse.SC_OK);
        }

        String token = request.getHeader("token");
        boolean isAuthSuccess = false;

        ServletContext context = request.getServletContext();
        ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(context);
        assert ctx != null;
        RedisUtil redisUtil = ctx.getBean(RedisUtil.class);

        if (Strings.isNullOrEmpty(token) || !redisUtil.exists(token, RedisPrefixKeyEnum.Sys.toString())) {
            log.warn("UnAuthorized url: " + request.getRequestURI());
            response.setStatus(HttpServletResponse.SC_OK);
            OutputStream outputStream = response.getOutputStream();

            outputStream.write(JSONObject.toJSONBytes(ApiRes.unAuthorized()));
            outputStream.flush();
            outputStream.close();
        } else {
            String idAndAccount = redisUtil.get(token, RedisPrefixKeyEnum.Sys.toString());
            String[] arr = idAndAccount.split(":");

            if (arr.length == 3) {
                UserData userData = new UserData();
                userData.setId(Integer.valueOf(arr[0]))
                        .setAccount(arr[1])
                        .setRoleId(Integer.valueOf(arr[2]));
                request.setAttribute("userData", userData);
                isAuthSuccess = true;
            }
        }

        if (isAuthSuccess) {
            var3.doFilter(var1, var2);
        }
    }
}

需要注意的是:前后端跨域的话,需要设置 response.setHeader("Access-Control-Allow-Origin", "*");

并做好 OPTIONS 类型的请求处理。

通过 token 验证之后,才会继续执行接下来的方法。

获取用户的信息也很简单,写一个接口 IBaseService.java 

public interface IBaseService {
    default UserData getUserData() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        UserData userData = (UserData) request.getAttribute("userData");
        return userData;
    }
}

服务层实现类中 implements IBaseService 就可以了:

String account = getUserData().getAccount();
Integer id = getUserData().getId();