TIP
http
请求是无状态的,也就是请求一次的状态并不会保存下来,有的网站的一些操作权限只支持登录的用户,以前做的登录就是请求一次API
,验证有没有这个用户,就假装登陆了。如何授权并保持这样的登录状态呢?
保存登录状态有大致两种方式,两者的区分点就是存不存在跨域的问题(跨域可以简单理解为,一个地址要请求另一个地址的内容)。
如果不存在跨域的问题,可以采用cookie
(在客户端记录状态)和session
(在服务端记录状态)的方式保存登录状态。客户端和服务器之间通常都是存在跨域的问题,因此就需要采用token
的方式保存登录状态。
# 注册流程
保存登录状态首先肯定是对于注册的用户来说的,先来看看注册流程,比较简单一点。对于注册页面不应该被拦截。由于token
中包含了密码,token
对于别人来说相当于明文,加密就是用的base64
,很好解开,所以一旦token
泄露就很危险了,这里密码和账户最好用BCR
加密一下。
# 登录流程
登录的流程,首先访问登录页面,把本地存储的文件都带上,主要就是看是否含有token
令牌,然后登录被拦截,如果未含有token
就直接输入账号密码,验证用户的身份,如果验证通过,就可以生成一个新的令牌,交给用户然后本地存储操作,如果用户名或者密码错误,就提示用户重新输入,再次验证身份。如果本地含有令牌,就验证令牌是否有效,如果令牌失效,就重新输入账号密码验证身份,颁发新的令牌。登录流程主要就是能得到一个能使用的令牌。
# 携带令牌请求API
这里就是需要注意的就是如何携带token
请求API
。
# 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)