TIP

http请求是无状态的,也就是请求一次的状态并不会保存下来,有的网站的一些操作权限只支持登录的用户,以前做的登录就是请求一次API,验证有没有这个用户,就假装登陆了。如何授权并保持这样的登录状态呢?

保存登录状态有大致两种方式,两者的区分点就是存不存在跨域的问题(跨域可以简单理解为,一个地址要请求另一个地址的内容)。

如果不存在跨域的问题,可以采用cookie(在客户端记录状态)和session(在服务端记录状态)的方式保存登录状态。客户端和服务器之间通常都是存在跨域的问题,因此就需要采用token的方式保存登录状态。

浏览器
浏览器
服务器
服务器
用户登录
用户登录
验证通过并生成相应Token
验证通过并生成相应Token
携带Token发送请求
携带Token发送请求
存储Token
存储Token
验证Token
验证Token
返回相应数据
返回相应数据
Viewer does not support full SVG 1.1

# 注册流程

保存登录状态首先肯定是对于注册的用户来说的,先来看看注册流程,比较简单一点。对于注册页面不应该被拦截。由于token中包含了密码,token对于别人来说相当于明文,加密就是用的base64,很好解开,所以一旦token泄露就很危险了,这里密码和账户最好用BCR加密一下。

开始
开始
填写个人信息
填写个人信息
检查用户是否存在
检查用户是否存在
Ok?
Ok?
对密码进行加密
对密码进行加密
保存账号密码
保存账号密码
结束
结束
BCR加密
BCR加密
Viewer does not support full SVG 1.1

# 登录流程

登录的流程,首先访问登录页面,把本地存储的文件都带上,主要就是看是否含有token令牌,然后登录被拦截,如果未含有token就直接输入账号密码,验证用户的身份,如果验证通过,就可以生成一个新的令牌,交给用户然后本地存储操作,如果用户名或者密码错误,就提示用户重新输入,再次验证身份。如果本地含有令牌,就验证令牌是否有效,如果令牌失效,就重新输入账号密码验证身份,颁发新的令牌。登录流程主要就是能得到一个能使用的令牌。


登 录

拦 截

令 牌
DB
DB





验证账号密码...



验证令牌...



生成令牌...
DB
DB
进入登录页面
进入登录页面
已登录或验证失败或令牌失效
已登录或验证失败或令牌失效
输入用户名密码
输入用户名密码
验证用户名密码
验证用户名密码
验证令牌
验证令牌
令牌失效
令牌失效
令牌有效
令牌有效
令牌失效
令牌失效



存在令牌...



本地存储...
新的令牌
新的令牌
验证失败
验证失败
验证通过
验证通过
验证失败
验证失败
Viewer does not support full SVG 1.1

# 携带令牌请求API

这里就是需要注意的就是如何携带token请求API





访

API
携带令牌访问API...
本地存储
本地存储



拦截令牌...



验证令牌...
令牌失效
令牌失效



取得令牌...
请求API
请求API
API
API...
获得数据
获得数据
令牌有效,fangwenAPI
令牌有效,fangwenAPI
Viewer does not support full SVG 1.1

# pom.xml中需要的依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.22</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mindrot/jbcrypt -->
<dependency>
    <groupId>org.mindrot</groupId>
    <artifactId>jbcrypt</artifactId>
    <version>0.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
      <exclusion>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
      </exclusion>
    </exclusions>
</dependency>

# 程序整体结构

程序整体结构放在这里,可以作为参考。

.
├── .idea
├── src
│   └── main
|       ├── java
|       |   └── org
|       |       └── example
|       |           ├── controller
|       |           |   └── UserController.java
|       |           ├── dao
|       |           |   └── UserMapper.java
|       |           ├── domain
|       |           |   └── User.java
|       |           ├── service
|       |           |   ├── UserService.java
|       |           |   └── Impl
|       |           |      └── UserServiceImpl.java
|       |           ├── utils
|       |           |   ├── JwtCfg.java
|       |           |   ├── JwtFilter.java
|       |           |   └── JwtUtils.java
|       |           └── Application.java
|       |           
|       |           
|       └── resources
|           ├── org
|           |   └── example
|           |       └──dao
|           |          └── UserMapper.xml
|           └── application.properties
├── target
└── pom.xml

# 编写UserController.java

这里包含一些API接口。只有user/login没有被拦截,其它接口都需要token认证才能够访问。

package org.example.controller;
import org.example.domain.User;
import org.example.service.UserService;
import org.example.utils.JwtUtils;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private User user;
    @GetMapping("test/findAll")
    public List<User> findAll(){
        System.out.println("Controller表现层,查询所有用户");
        System.out.println(user);
        List<User> all = userService.findAll();
        for (User user : all) {
            System.out.println(user);
        }
        return all;
    }
    @PostMapping("test/addUser")
    public String addUser(User user){
            user.setPassword(BCrypt.hashpw(user.getPassword(),BCrypt.gensalt()));
            System.out.println("Controller表现层,增加用户");
            userService.addUser(user);
            return "添加用户成功";
    }
    @PostMapping("/login")
    public String login(HttpServletResponse response, User user) {
        // 查询数据库,查找此用户输入信息是否正确
        User users = null;
        users = userService.findUser(user);
        System.out.println(users.getUsername()+":"+users.getPassword());
        if(!BCrypt.checkpw(user.getPassword(),users.getPassword())){
            return "你输入的用户名或密码有误";
        }
        else{
            System.out.println(user);
            String token = JwtUtils.geneJsonWebToken(users.getUsername(), users.getPassword());
            // 将token放在响应头
            response.setHeader("Authorization", "Bearer "+token);
            return "Bearer "+token;
        }
    }
    @RequestMapping("test/secure/check")
    public String check(){
        return "登录成功";
    }
}

# 编写UserMapper.java

这里是持久层的代码,创建访问数据库的接口,需要使用注解或者和一个同名的xml文件配合使用。访问数据库需要实现这些接口的功能。

package org.example.dao;
import org.example.domain.User;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserMapper {
    //查找所有的用户
    List<User> findAll();
    //增加一个用户
    void addUser(User user);
    //查找一个用户
    User findUser(User user) ;
}

# 编写User.java

实体类,一般和数据库的属性是一致的,可以将数据库的一张表封装成一个实体类。

package org.example.domain;
import org.springframework.context.annotation.Configuration;
import java.io.Serializable;
@Configuration
public class User implements Serializable {
    //用户名
    private String username;
    //密码
    private String password;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    @Override
    public String toString() {
        return "User{" +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

# 编写UserService.java

业务层接口代码,和表现层接口代码一样,配合使用。

package org.example.service;
import org.example.domain.User;
import java.util.List;
public interface UserService {
    List<User> findAll();
    void addUser(User user);
    User findUser(User user) ;
}

# 编写UserServiceImpl.java

实现业务层接口功能,供表现层调用。

package org.example.service.Impl;
import org.example.dao.UserMapper;
import org.example.domain.User;
import org.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public List<User> findAll() {
        System.out.println("Service业务层,查询所有用户");
        return userMapper.findAll();
    }
    @Override
    public void addUser(User user) {
        System.out.println("Service业务层,保存用户");
        userMapper.addUser(user);
    }
    @Override
    public User findUser(User user) {
        return userMapper.findUser(user);
    }
}

# 编写JwtCfg.java

JWT的配置类,例如可以配置具体的拦截的路径,现在我也只会配置这个^_^。

package org.example.utils;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JwtCfg {
    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        final FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new JwtFilter());
        // 对/user/test/*下的url进行拦截验证
        registrationBean.addUrlPatterns("/user/test/*");
        return registrationBean;
    }
}

# 编写JwtFilter.java

这个类的作用是拦截器(Filter),拦截的路径就是在JwtCfg.java中配置,可以获取到请求头中的令牌,也就是token,这个类我感觉也是比较核心的一个类,对于携带token的用户,需要将token拿过来,然后解码,获取到里面的值。

token就是类似这样的一串代码,前面的Bearer不包含在token中。当token过期的时候,JWT会自动判断其失效的,不用自己手动配置。

Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJOYW1lIjoiMTExIiwiaWF0IjoxNTkxMDA3Njk4LCJleHAiOjE1OTEwOTQwOTh9.fidk9TBllhA_lb9CNODCf3HD4dp-NNsyOdPU0O3Wnhw

解码之后就变成了这样

{sub=admin, userName=111, iat=1591007698, exp=1591094098}

里面也还可以包含很多这样的键值对,下面是它们的含义。

TIP

JWT标准里面定好的claim有:

iss(Issuser):代表这个JWT的签发主体;

sub(Subject):代表这个JWT的主体,即它的所有人;

aud(Audience):代表这个JWT的接收对象;

exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;

nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;

iat(Issued at):是一个时间戳,代表这个JWT的签发时间;

jti(JWT ID):是JWT的唯一标识。

package org.example.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtFilter extends GenericFilterBean {
    /**
     * 秘钥
     */
    public static final String SECRET_KEY = "secretkey";
    @Override
    public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain)
            throws IOException, ServletException{
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;
        // 从request中获取authorization
        final String authHeader = request.getHeader("authorization");
            // 判断token是否是用Bearer 开头的
            String header = "Bearer ";
            if (authHeader == null || !authHeader.startsWith(header)) {
                //没有token或者token格式不对则抛一个异常,
                //也可以自己设置返回的状态码
                //response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
                //在前端取得这样的状态码则说明请求失败
                throw new ServletException("Missing or invalid Authorization header");
            }
            else {
                // 然后从授权处获取JWT令牌,去掉前面的Bearer
                final String token = authHeader.substring(7);
                try {
                    // 使用JWT解析器检查签名是否与Key "secretkey "有效。
                    final Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
                    System.out.println(claims);
                    System.out.println(claims.getExpiration().getTime());
                    System.out.println(System.currentTimeMillis());
                    // 在请求标题中添加claims
                    request.setAttribute("claims", claims);
                } catch (Exception e) {
                    throw new ServletException("Invalid token");
                }
        }
        chain.doFilter(req, res);
    }
}

# JwtUtils.java

这个是生成token的类。

package org.example.utils;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;
import java.util.Date;
public class JwtUtils {
    public static final String SUBJECT = "admin";
    /**
     * 过期时间,毫秒,一天
     *
     */
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    /**
     * 秘钥
     */
    public static final String SECRET_KEY = "secretkey";
    /**
     * 生成jwt
     * @param username
     * @param password
     * @return
     */
    public static String geneJsonWebToken(String username, String password) {
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            return "用户名或密码不能为空";
        }
        return Jwts.builder().setSubject(SUBJECT)
                .claim("userName", username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
    }
}

# 编写Application.java

SpringBoot的主入口类。

package org.example;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement
@MapperScan("org.example.dao")
class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

# 编写UserMapper.xml

这个相当于持久层的接口UserMapper.java实现类,里面可以写一些SQL语句,对数据库进行CRUD。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.dao.UserMapper">
    <!-- 开启二级缓存 -->
    <cache/>
    <select id="findAll" resultType="user">
      select * from user
    </select>
    <insert id="addUser" parameterType="user">
        insert into User (username,password) values (
        #{username},#{password}
        );
    </insert>
    <select id="findUser" parameterType="user" resultType="user">
        select * from user where username=#{username};
    </select>
</mapper>

# application.properties

springboot的配置文件。

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/srcrs?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
mybatis.mapper-locations=classpath:org/example/dao/*.xml
mybatis.type-aliases-package=org.example.domain

至此就已经编写完了。目前可以做的就是通过user/login接口获得token令牌,然后通过这个令牌就可以访问其它所有已经定义好的接口,实现了简单的身份认证。但是这也还是不够用的,例如有些接口是不可以被外界所使用的,尤其是对一些重要的数据,所以接下来还得需要一个安全框架,实现认证与授权,通过定义一个个角色,对这些角色授予访问部分API接口的权限,能够让API接口更加的安全,现在这个样子是不行的,计划准备使用spring security来实现。

还有需要注意一些坑,最好要保证包的版本一直,否则可能会出一些莫名其妙的问题,例如jwt包,我开始选择的是0.11.1的这个包,但是不知道为什么老师报错,然后我就换成了0.9.0就好了,很奇怪。

另外就是做接口测试,可以在chrome中装一个插件Tabbed Postman - REST Client (opens new window),这个插件就可以做接口测试,还比较好用。

# 参考链接

Spring Boot整合JWT实现认证 (opens new window)

前后端分离后如何实现登录?前端基于vue、axios,后端基于springSecurity、JWT、BCR算法加密解密 (opens new window)

JWT在身份认证方面的应用 (opens new window)