本节包含使用jwt验证方式(适合前后端分离)代替传统的session验证、实现登录、获取信息、以及注册三个后端API
使用jwt验证方式(适合前后端分离)代替传统的session验证
传统的验证登录模式
- 公开页面:输入url就可以直接访问,如
login
页面 - 授权页面:登录之后才可以访问
当前实现的效果就是,登录页面输入账号密码,只要不关闭浏览器,则可以实现访问其他授权页面。具体的流程是:即将输入的账号密码和数据库中的内容做比对,如果和数据库内容一致,就会给用户发送一个sessionId
,用户会将sessionId
存储到cookie
里面。之后再访问授权页面的时候,就会判断sessionId
是否有效
Jwt验证模式
- 容易实现跨域
- 不需要在服务器端存储
对比于传统模式将所有的sessionId
换成jwt token
access token
refresh token
过程:通过一个login页面获取一个token,将其存入浏览器中,当访问授权页面的时候都会带上这个token,先验证这个token(包含userId)是否合法,根据userId到数据库中查询信息,提取到上下文中,访问授权的方法。
以上是原理,操作如下
- 添加
jwt
依赖,官网搜索jwt
包括jjwt-api
jjwt-impl
jjwt-jackson
- 实现
utils.JwtUtil
类,为jwt
工具,用来创建、解析Jwt token
, 如果JwtUtil
爆红,可以降低到0.11.5
版本试一下。 - 实现
config.filter.JwtAuthenticationTokenFilter
类,用来验证jwt token
,如果验证成功,则将User信息注入上下文中 - 配置
config.SecurityConfig
类,放行登录、注册等接口
首先去Maven仓库查找并添加以下依赖: JetBrains Java Annotations
jjwt-api
jjwt-impl
jjwt-jackson
然后实现utils.JwtUtil
类(utils
包在 com.kob.backend
下,放在哪个包下都是个人习惯),是JWT
工具类,用来创建、解析jwt token
package com.kob.backend.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // token的有效期设置为14天
public static final String JWT_KEY = "IVK157AXCZSChcwW23AUvayrXYhgcXAHKBMDziw17dW"; // 密钥,自己随便打,但是长度要够长,否则会报错
public static String getUUID() { // 生成一个随机的UUID并去掉其中的"-"
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); // 使用Base64解码预设的JWT_KEY
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256"); // 使用这个解码后的密钥生成一个HmacSHA256算法的SecretKey
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis; // 计算出token的过期时间
Date expDate = new Date(expMillis);
return Jwts.builder()
.id(uuid)
.subject(subject)
.issuer("sg")
.issuedAt(now)
.signWith(secretKey)
.expiration(expDate);
}
public static String createJWT(String subject) { // 创建一个JWT。
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact(); // 将其转换为一个紧凑的URL安全的JWT字符串
}
public static Claims parseJWT(String jwt) throws Exception { // 解析一个JWT
SecretKey secretKey = generalKey();
return Jwts.parser()
.verifyWith(secretKey) // 使用生成的密钥来验证JWT的签名
.build()
.parseSignedClaims(jwt)
.getPayload(); // 解析JWT并返回其payload(载荷)部分
}
}
接下来需要实现com.kob.backend.config.filter.JwtAuthenticationTokenFilter
用来验证jwt token
,如果验证成功,则将User
信息注入到上下文当中
package com.kob.backend.config.filter;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization"); // 从请求头中获取名为"Authorization"的字段
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) { // 这个字段应该包含一个以"Bearer "开头的JWT
filterChain.doFilter(request, response); // 将请求传递给下一个过滤器或处理器
return;
}
token = token.substring(7); // 跳过"Bearer "共7个字符
String userid;
try {
Claims claims = JwtUtil.parseJWT(token); // 解析JWT,获取JWT的载荷
userid = claims.getSubject(); // 从载荷中获取"subject",这个"subject"应该是用户ID
} catch (Exception e) {
throw new RuntimeException(e);
}
User user = userMapper.selectById(Integer.parseInt(userid)); // 查询数据库获取用户信息
if (user == null) {
throw new RuntimeException("用户未登录");
}
UserDetailsImpl loginUser = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 将其设置到Spring Security的上下文中
filterChain.doFilter(request, response); // 将请求传递给下一个过滤器或处理器
}
}
最后配置com.kob.backend.config.SecurityConfig
类,放行登录、注册等接口,因为用户没有登录的时候,需要先访问这些页面才能进行登录。
package com.kob.backend.config;
import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception { // AuthenticationManager用于处理身份验证
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception { // 配置HttpSecurity
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/account/login/", "/user/account/register/").permitAll() // 需要公开的链接在这边写即可
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
编写API,实现验证登录API
实现了配置之后就可以创建后端的API了,在此之前,给数据库中的user
表添加一列photo
用来存储用户的头像链接,类型为varchar(1000)
,然后去pojo.User
添加一个字段
private String photo;
这里主要设置登录验证,所以编写如下三个API
- 实现
/user/account/token/
:验证用户名密码,验证成功后返回jwt token
(令牌) - 实现
/user/account/info/
:根据令牌返回用户信息 - 实现
/user/account/register/
:注册账号
·SpringBoot
编写一个API
的一般步骤:service
里面接口,需要在service.impl
中写对应接口的实现类,然后编写controller
里面的类,controller
用来调用service
- 将数据库中的id域变为自增
- 在数据库中将id列变为自增
- 在
pojo.User
类中添加注解:@TableId(type = IdType.AUTO)
编写第一个API:/user/account/login/
,功能是验证用户名和密码,验证成功之后返回jwt token
在service
包下创建user.account
包,表示用户账号相关的API,然后创建LoginService
接口
package com.kob.backend.service.user.account;
import java.util.Map;
public interface LoginService {
Map<String, String> login(String username, String password);
}
然后在service.impl
包下创建user.account
包,然后在这个包下创建LoginServiceImpl
类用来实现LoginService
接口
package com.kob.backend.service.impl.user.account;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.service.user.account.LoginService;
import com.kob.backend.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service // 注入到Spring中,未来可以用@Autowired注解将该类注入到某个其他类中
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public Map<String, String> login(String username, String password) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password); // 需要先封装一下,因为数据库中存的不是明文
Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 验证是否能登录,如果失败会自动处理
UserDetailsImpl loginUser = (UserDetailsImpl) authenticate.getPrincipal();
User user = loginUser.getUser(); // 将用户取出来
String jwt_token = JwtUtil.createJWT(user.getId().toString()); // 将用户的ID转换成jwt_token
Map<String, String> res = new HashMap<>();
res.put("result", "success");
res.put("jwt_token", jwt_token);
return res;
}
}
最后实现controller
模块,先把 controller.user
包下的 UserController
类删了,这是之前学习数据库操作与调试用的,然后创建一个account
包,在包中创建LoginController
类:
package com.kob.backend.controller.user.account;
import com.kob.backend.service.user.account.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class LoginController {
@Autowired // 将接口注入进来,这就是Spring的IoC依赖注入特性
private LoginService loginService;
@PostMapping("/user/account/login/") // 登录采用POST请求,不是明文传输,较为安全
public Map<String, String> login(@RequestParam Map<String, String> data) { // 将POST参数放在一个Map中
String username = data.get("username");
String password = data.get("password");
return loginService.login(username, password);
}
}
后端没有成功实现,还是跨域验证的问题
然后通过前端进行测试
直接在前端项目的 App.vue
文件中编写临时调试代码,使用 Ajax
发出 POST
请求:
知识点的补充学习
HTTP协议中的GET和POST是两种常用的请求方法,它们在Web开发中有着不同的用途和特点:
Post请求和Get请求
-
GET请求:
- 用途:用于请求从服务器获取数据。通常用于查询字符串参数,如搜索或过滤数据。
- 数据传输:数据通过URL传递,附加在请求的URL之后。
- 安全性:由于数据在URL中可见,因此不安全,不应用于传输敏感信息。
- 缓存:GET请求可以被缓存,这意味着相同的请求可以快速响应,提高效率。
- 书签:可以被保存为书签。
- 历史:可以在浏览器历史中保留。
- 长度限制:URL长度有限制,因此GET请求传输的数据量有限。
-
POST请求:
- 用途:用于向服务器提交数据,通常用于表单提交或上传文件。
- 数据传输:数据在请求体(body)中传输,不在URL中。
- 安全性:比GET更安全,因为数据不在URL中显示。
- 缓存:POST请求不会被缓存。
- 书签:不能被保存为书签。
- 历史:不会在浏览器历史中保留。
- 长度限制:没有长度限制,可以传输大量数据。
在选择使用GET还是POST时,通常考虑以下因素:
- 如果需要获取数据,并且数据量不大,可以使用GET。
- 如果需要向服务器提交数据,或者数据量较大,应该使用POST。
- 对于敏感数据,应避免使用GET,因为它可能会在URL中暴露数据。
在实际开发中,还应考虑其他HTTP方法,如PUT(更新资源)、DELETE(删除资源)、PATCH(部分更新资源)等,根据具体需求选择合适的方法。
jwt的介绍
JSON Web Token(JWT)是一种用于在网络应用环境间传递声明的紧凑、URL安全的开放标准(RFC 7519)。它被设计为紧凑且安全的,适用于分布式站点的单点登录(SSO)场景。JWT的声明通常用于在身份提供者和服务提供者间传递被认证的用户身份信息,以便从资源服务器获取资源,也可以增加一些额外的业务逻辑所需的声明信息。JWT可以被用于认证,也可以被加密。
JWT由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。头部包含两部分信息:令牌的类型(通常是JWT)和使用的加密算法(如HMAC SHA256或RSA)。载荷部分存放有效信息,包括注册的声明(如iss、exp、sub、aud等)、公共的声明和私有的声明。签名部分用于验证发送请求者身份,由前两部分加密形成。JWT的优点包括体积小、传输速度快,可以通过URL、POST参数或HTTP头部等方式传输,支持跨域验证,适用于单点登录,且由于其自包含性,可以有效减少服务器查询数据库的次数。然而,JWT也有其局限性,如不能存储敏感信息,因为其载荷是使用Base64编码的,没有加密,且JWT一旦签发,不能被撤销,只能等到过期。在实际应用中,JWT可以用于身份验证、信息交换、单点登录和微服务架构等场景。为了保证安全性,建议不要在JWT中存放敏感信息,设置合理的有效期,确保传输过程安全,并考虑在应用程序层面增加额外的安全措施,如黑名单机制等。