springboot实现登录demo
实现简单的登录功能
实体类
定义实体类为User3类。
使用@Data:提供类的get,set,equals,hashCode,canEqual,toString方法;
使用@AllArgsConstructor:提供类的全参构造
使用@NoArgsConstructor:提供类的无参构造
类代码如下
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User3
{
private Integer id;
private String userName;
private String password;
}
JWT核心类
JWT认证流程如图所示
可见服务器端需要做两件事,一是根据用户信息创建对应的JWT密钥,另一个是根据对应的密钥解码用户信息。
JWT token主要由三个部分组成,主要包括Header,Payload,Signature。通过“."间隔开来。
- Header:包括两部分信息,令牌的类型(typ),签名算法(alg)。
- Payload:也称为声明(Claims),包含JWT的主要信息,主要是用户身份信息,权限等,是一个JSON对象。
- Signature:使用头部使用的算法和密钥对头部和载荷进行签名所生成的一部分,用来验证真实性和完整性。
签名主要是先将头部和载荷转换为Json格式,并进行base64编码,最后用.拼接在一起
如
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "1234567890", "name": "John Doe", "admin": true}
转换后为
Encoded Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Encoded Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
使用指定的算法对Encoded Header.Payload进行签名。最后再拼接在一起
验证时使用JWTVerifier实例,使用 HMAC256 算法和指定的密钥(SECRET)来验证签名。
主要代码如下
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
/**
* 密钥
*/
private static final String SECRET = "my_secret";
/**
* 过期时间
**/
private static final long EXPIRATION = 1800L;//单位为秒
/**
* 生成用户token,设置token超时时间
*/
public static String createToken(User3 user) {
//过期时间30分钟
Date expireDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
String token = JWT.create()
.withHeader(map)// 添加头部
//可以将基本信息放到claims中
.withClaim("id", user.getId())//userId
.withClaim("userName", user.getUserName())//userName
.withClaim("password", user.getPassword())//password
.withExpiresAt(expireDate) //超时设置,设置过期的日期
.withIssuedAt(new Date()) //签发时间
.sign(Algorithm.HMAC256(SECRET)); //SECRET加密
System.out.println("生成的token:"+token);
return token;
}
/**
* 校验token并解析token
*/
public static Map<String, Claim> verifyToken(String token) {
DecodedJWT jwt = null;
System.out.println("识别的token:"+token);
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
jwt = verifier.verify(token);
} catch (Exception e) {
logger.error(e.getMessage());
logger.error("token解码异常");
return null;
}
return jwt.getClaims();
}
}
JWT过滤器
使用过滤器可以提供一种轻量级,安全,高效的方式来处理身份验证和授权,可以简化开发流程。
可以避免服务器每次请求都进行状态管理,无需每次都查询数据库验证用户身份或访问权限。
使用过滤器前我们首先要进行注册
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<JwtFilter> jwtFilter() {
FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtFilter());
registrationBean.addUrlPatterns("/secure/*");
return registrationBean;
}
}
由于用户在每次请求时都会带上Authorization的token,所以要提取出对应的token,识别出用户对应的信息后再进行下一步处理。
整个过程处于过滤器链中,对于 OPTIONS 请求,直接放行并调用 chain.doFilter(request, response);
过滤器的代码如下。
public class JwtFilter implements Filter
{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
String temp = request.getHeader("authorization");
System.out.println("temp:" + temp);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
final String token = request.getHeader("authorization");
System.out.println("token:" + token);
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
chain.doFilter(request, response);
}
else {
if (token == null) {
response.getWriter().write("没有token!");
return;
}
Map<String, Claim> userData = JwtUtil.verifyToken(token);
if (userData == null) {
response.getWriter().write("token不合法!");
return;
}
Integer id = userData.get("id").asInt();
String userName = userData.get("userName").asString();
String password= userData.get("password").asString();
//拦截器 拿到用户信息,放到request中
request.setAttribute("id", id);
request.setAttribute("userName", userName);
request.setAttribute("password", password);
chain.doFilter(req, res);
}
}
@Override
public void destroy() {
}
}
或许会有个疑问,那就是这里代码也调用了chain.doFilter(req, res);会导致递归等问题吗。
其实并不会,每个过滤器都会调用chain.doFilter(req, res)方法把请求传递给下一个过滤器,直到最后一个过滤器调用目标Servlet。整个过程处于线性并且有终点。
一般来说,过滤器链的工作流程如下:
- 过滤器链初始化:当一个请求到达时,服务器根据配置初始化过滤器链。
- 过滤器链顺序执行:请求进入第一个过滤器,第一个过滤器在逻辑中调用chain.doFilter(request, response); 将请求传递给下一个过滤器。
- 传递到目标资源:过程一直持续,直到最后一个过滤器调用chain.doFilter(request, response)传递给最终的目标Servlet或JSP页面来处理。
- 响应返回:目标资源生成响应后,响应会逆向通过所有过滤器回传给客户端,每个过滤器都有机会在返回路径上再次处理响应。
下面以一个例子来说明
public class Filter1 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("Filter1: before chain");
chain.doFilter(request, response); // 将请求传递给下一个过滤器
System.out.println("Filter1: after chain");
}
}
public class Filter2 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("Filter2: before chain");
chain.doFilter(request, response); // 将请求传递给下一个过滤器
System.out.println("Filter2: after chain");
}
}
public class TargetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.getWriter().write("TargetServlet response");
}
}
当请求到达时,控制流执行如下:
Filter1 执行前部分代码(Filter1:before chain)。
Filter1 调用 chain.doFilter(request, response); 将请求传递给 Filter2。
Filter2 执行前部分代码(Filter2: before chain)。
Filter2 调用 chain.doFilter(request, response); 将请求传递给 TargetServlet。
TargetServlet 生成响应。
响应传回 Filter2,执行后部分代码(Filter2: after chain)。
响应传回 Filter1,执行后部分代码(Filter1: after chain)。
最终响应传回给客户端。
如果过滤器链中的最后一个过滤器调用了chain.doFilter(request, response);,请求将被传到目标资源。
解码返回响应
最后为了查看解码是否正确,还需要查看解码后的用户信息
编写类代码如下
@RequestMapping("/secure/getUserInfo")
@UnInterception
public JSONObject login(HttpServletRequest request) {
// final String token = request.getHeader("authorization");
// System.out.println("token:" + token);
Integer id = (Integer) request.getAttribute("id");
System.out.println(id);
String userName = request.getAttribute("userName").toString();
String password = request.getAttribute("password").toString();
HttpServletResponse response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
assert response != null;
response.setCharacterEncoding("UTF-8");
JSONObject test = new JSONObject();
test.put("id",id);
test.put("userName",userName);
test.put("password",password);
System.out.println("当前用户信息id=" + id + ", userName=" + userName + ", password=" + password);
return test;
}
这里的request是已经完成了过滤后的请求,注意要设置字符编码为UTF-8,否则可能会返回乱码。
static Map<Integer, User3> userMap = new HashMap<>();
static {
User3 user1 = new User3(1,"张三","123456");
userMap.put(1, user1);
User3 user2 = new User3(2,"李四","123123");
userMap.put(2, user2);
}
/**
* 模拟用户 登录
*/
@RequestMapping(value = "/login", method = RequestMethod.GET)
@UnInterception
public String login(@RequestParam String userName, @RequestParam String password)
{
System.out.println("userName: " + userName + ", password: " + password);
for (User3 dbUser : userMap.values()) {
if (dbUser.getUserName().equals(userName) && dbUser.getPassword().equals(password)) {
log.info("登录成功!生成token!");
String token = JwtUtil.createToken(dbUser);
return token;
}
}
return "";
}
测试结果
在Apifox上测试后结果如下。
可以看到正常发送了请求并且解码成功,而且编码是UTF-8