引言
在业务系统开发中,用户身份验证是保障系统服务安全性的基石。无论是社交网络、电商平台还是企业级应用,都需要确保用户的访问权限与行为与其真实身份相符。为了达到这一目的,我们通常采用了一系列技术手段来管理用户会话并验证其身份,其中最常见的是Cookie和Session机制。但是,在近年来随着微服务以及API驱动架构的发展,JSON Web Tokens(JWT)作为一种轻量级的身份验证方案得到了广泛的关注和应用。
那么,JWT究竟是什么?它的原理是如何运作的?又为何在已有Cookie和Session的基础上,仍需要引入JWT作为身份验证的一种新方法呢?
Cookie和Session
Cookie是什么?
Cookie是Web开发中一种关键的客户端存储技术,它是由Web服务器在用户访问时生成并发送至用户浏览器的微型数据包,以文本文件的形式储存在用户的设备上。浏览器根据HTTP协议的规定,在之后对同一服务器的所有请求中自动附带这些Cookie信息。
服务器通过在HTTP响应头中设置Set-Cookie字段,来指示浏览器保存具有特定名称、值和其他配置参数(例如有效期、安全选项、路径限制、域名限制等)的Cookie。这样,每一个Cookie实质上就是一对键值对,帮助服务器识别和区分不同的用户及其在网站上的交互历史。
Cookie的一个核心应用是在用户身份验证和会话管理方面,它可存储用户的会话ID,使得用户在不同页面间切换或重新打开网站时,服务器能够基于这个ID重建用户的会话状态,实现无缝登录和个性化体验。此外,Cookie还广泛应用于记录用户喜好、统计分析用户行为、广告定向投放等方面,以提升用户体验和服务质量。但是,由于Cookie可能涉及用户隐私,现代浏览器和相关法规越来越重视对Cookie使用的透明度和用户控制权。
Seeion是什么?
Session作为一种服务器端机制,旨在管理和维护用户在其交互期间的状态信息,这些信息被安全地存储在服务器内存或者持久化存储(如数据库)中,与仅在客户端存储的Cookie形成了鲜明对比。每当用户开始与服务器建立联系时,服务器会初始化一个新的Session对象,并将其状态数据保存在服务器内部资源中。
一旦Session建立,服务器会产生一个独一无二的Session标识符——Session ID,该ID随后会通过HTTP响应中的Cookie(通常命名规则包括但不限于JSESSIONID)下发至客户端浏览器进行存储。此后,用户的每一次请求都将携带着这个Session ID,服务器通过解析请求中的Session ID,能够在自身的Session存储区域内检索到相对应的Session对象,从而实时获取并更新用户会话的具体状态。
Session机制广泛应用在诸如认证授权场景中,用来持久化登录用户的凭证和权限等敏感信息,确保用户在浏览网站的不同页面时仍能维持登录状态和个性化体验。此外,Session还能用于暂存用户会话过程中的临时数据,比如购物车的内容、网页表单填写的中间状态等,直至会话自然终止(如超时)、用户主动登出或清理Session时,这些数据才会失效。
但是虽然Cookie和Session在会话管理中一直使用,但它们存在一些缺点如下:
- Cookie数量和大小限制:浏览器对Cookie的数量和大小有限制,可能导致用户数据存储受限。
- 服务器资源占用:Session需要在服务器端存储大量用户会话信息,随着并发用户数的增长,可能会造成服务器内存资源紧张。
- 跨域问题:Cookie默认遵循同源策略,不便于跨域共享会话状态。
- 分布式系统中的Session同步:在分布式环境或集群部署的情况下,Session数据需要在各服务器之间同步,增加了系统的复杂性。
为解决上述会话管理中的缺点,以及对无状态服务、更好的扩展性和安全性要求,JWT这样的新型身份验证技术就这样诞生了。JWT通过将用户信息加密打包成Token,让客户端自行携带认证信息,从而实现了服务器的无状态化。
JWT是什么?
JSON Web Tokens (JWT) 是一种开放标准(RFC 7519),定义了一种紧凑、自包含的方式来安全地在各方之间传输信息。JWT主要由三个部分组成,即Header(头部)、Payload(载荷)和Signature(签名),这三部分之间通过.分隔。
JWT的组成
JWT的内容采用Base64编码,可以直接嵌入到HTTP请求头或者URL查询参数中,因其具有可读性、自包含和防篡改的特点而广泛应用在身份验证和授权场景中。
Header
描述了所使用的JWT类型(通常为JWT)以及签名算法(如HS256、RS256等)。
• typ: 表明这是一个JWT(固定为 "JWT")。
• alg: 指定用于签署JWT的算法,如 "HS256"(HMAC SHA-256)或 "RS256"(RSA SHA-256)等。
{
"alg": "HS256",
"typ": "JWT"
}
Payload
包含了实际要传递的数据,可以是任意的JSON对象,包含一组称为声明(claims)的数据。一般为用户身份信息(如用户ID、角色、权限等)和其他自定义声明,还包含一个exp(Expiration Time)字段来设置JWT的有效期以及其他元数据。声明分为三种类型:
- Registered Claims(注册声明) 这些是预先定义好的标准声明,虽然不是必须的,但在JWT规范中建议使用。它们提供了一套通用的信息,有助于JWT的标准化处理。常见的注册声明包括:
• iss(issuer):签发JWT的实体。
• sub(subject):JWT所面向的用户或主题。
• aud(audience):预期接收JWT的受众。
• exp(expiration time):JWT过期时间,在此时间之后JWT应被视为无效。
• nbf(not before):JWT生效时间之前,不应被接受处理的时间点。
• iat(issued at):JWT的创建时间。
• jti(JWT ID):JWT的唯一标识符,可用于防止重放攻击。
2. Public Claims(公共声明) 公共声明是预留用于行业共识的标准声明,虽然目前并未正式注册到IANA JSON Web Token Registry,但可以在IANA JSON Web Token Registry 查看已注册的声明集。如果没有官方注册,但多个项目间需要共享相同的声明,可以选择将声明名称以特定前缀(如 urn:<namespace>:<name>)进行命名,避免冲突。
3. Private Claims(私有声明) 私有声明是用于应用内部约定的自定义声明,我们可以根据自己业务需求自由定义。例如,可以包含用户ID (userId)、用户角色 (role)、邮箱地址 (email) 等任何想要在JWT中传递的信息。
示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"jti": "1pmysetgujcoden",
"exp": 1516242622,
"scope": [
"read",
"write"
]
}
Payload部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!
Signature
Signature部分用于确保JWT在传输过程中没有被篡改,它是通过对前两部分(Header和Payload编码后的字符串)使用Header中指定的加密算法以及一个共享或私有的密钥进行签名计算得到的。签名确保了只有知道该密钥的实体才能创建有效的JWT,并且任何人都可以验证JWT的完整性和来源的真实性。
生成签名的计算公式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,这个字符串就是JWT 。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwianRpIjoiMXBteXNldGd1amNvZGVuIiwiZXhwIjoxNTE2MjQyNjIyLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.eW91cmUtYXV0aG9yaXphdGlvbi1zaWduYXR1cmU=
对于这段字符串,我们在JWT官网中,使用它的解码器进行解码,就可以得到Header、Payload、Signature这三部分。
图片
如何基于JWT进行身份验证?
JWT生成
服务器首先构建JWT的Header和Payload,并分别进行Base64编码。使用Header中指定的签名算法(比如HMAC SHA-256或RSA)对编码后的Header和Payload进行签名,生成Signature。将Header、Payload和Signature连接在一起,形成完整的JWT字符串。
JWT发放
用户通过用户名/密码登录后,服务器验证用户身份无误,便生成JWT并将其返回给客户端。客户端可以将JWT存储在浏览器的LocalStorage、SessionStorage中,或者作为Bearer Token放在Authorization请求头中。
这里需要注意,将JWT存储在浏览器的localStorage中,相较于存储在Cookie中,可以降低CSRF(跨站请求伪造)的风险。因为CSRF攻击依赖于浏览器自动附带在请求中的Cookie,而localStorage中的数据不会自动包含在普通的HTTP请求头部。
至于携带JWT的方式,建议将JWT放在HTTP Header的Authorization字段中,采用Bearer Token的形式:
Authorization: Bearer <JWT>
这种做法允许在无状态的RESTful API中方便地进行身份验证,服务器可以根据请求头中的JWT来验证请求发起者的身份和权限。同时,这种方式也便于实现JWT的刷新与撤销。
JWT使用
在后续的请求中,客户端将JWT随HTTP请求一起发送给服务器。服务器接收到JWT后,对其进行解码和验证Signature,如果Signature正确,则说明JWT未被篡改,并且仍处于有效期内。根据Payload中的信息,服务器可以确定用户身份以及相关权限,从而做出相应处理。
JWT过期与刷新
JWT有一定的生命周期,过了指定的exp字段时间后,服务器不再接受该JWT。对于长时间交互的场景,可以设计Refresh Token机制,当JWT即将过期时,客户端使用Refresh Token去服务器申请新的JWT,以延长用户会话的有效期。
图片
JWT安全性
JWT如何防止被篡改?
JWT通过签名(Signature)来防止被篡改。数字签名的机制确保了JWT的内容一旦被篡改,服务端就能检测出来,从而拒绝非法的请求。
当客户端带着JWT向服务器发送请求时,服务器会先将接收到的JWT按.分割成Header、Payload和Signature三部分。服务器使用与生成签名时相同的密钥和算法,根据接收到的Header和Payload重新计算签名。如果重新计算出的签名与接收到的Signature一致,那么可以认为JWT在传输过程中未被篡改;如果不一致,则说明JWT已被篡改。
当然,JWT通过这种方式防止被篡改的一个大前提就是,秘钥(Secret Key)是安全的,如果秘钥泄漏,那就可以更改Header、Payload,然后利用这个秘钥生成一个Signature就可以了,那么就不安全了。所以,我们一定要确保服务端使用的密钥是安全的,并且严格保密,不对外泄露。
如何加强JWT的安全性?
作为使用者,防止JWT被篡改的主要措施并不直接体现在客户端的操作上,而是依赖于服务端的安全设计和实施。我们可以通过以下方面考虑,加强JWT的安全性:
1. 服务端安全策略:首先我们必须确保服务端使用的密钥是安全的,并且严格保密,不对外泄露。密钥用于签署和验证JWT,一旦密钥泄露,可能导致JWT被伪造或篡改。并且还要采用安全强度足够高的签名算法,例如HS256、RS256等,这些算法能有效地确保JWT的完整性。
2. 正确的签名流程:确保所有签发的JWT都经过服务器端签名,并且每次验证JWT时都要使用相同的密钥和算法进行验证。
3. 设置合理的Token有效期:设置JWT的有效期(exp claim)限制,避免JWT长时间有效,减少恶意攻击者篡改JWT后继续使用的机会。
4. 严谨的身份验证逻辑:在服务端验证JWT时,除了验证签名之外,还应对Payload中的其他重要声明(如用户ID、权限、过期时间等)进行校验。
5. 客户端存储方式:尽管JWT被篡改的风险主要集中在网络传输阶段,但在客户端存储JWT时,推荐使用localStorage而非Cookie(尤其是考虑到防止CSRF攻击),同时也可以考虑对敏感信息加密存储。
JWT相较于Cookie和Session的优缺点
JWT的优点
JWT是无状态的,这样服务器不需要存储会话状态信息,减轻了服务器端的存储负担,在分布式环境下使用JWT可以在集群内的任何节点验证,无需保持会话状态同步,JWT还可以轻易地在不同域名的服务之间传递,适用于微服务架构和跨域应用。因为它包含了所有必要的用户信息,所以在服务间跳转时无需再次验证用户身份,每个请求都包含了认证所需要的所有信息,减少了服务器查询数据库或存储服务以验证用户状态的次数,从而提高了效率。
JWT的缺点
JWT中的数据默认情况下是Base64编码的,虽然可以加密但并非强制要求,这使得在不加密的情况下,JWT不适合存储敏感信息。相比而言,Session存储在服务器端,安全性更高。如果JWT被盗取,黑客可以在有效期内持续使用,直到token过期或被撤销。另外JWT的体积相对较大,特别是当包含更多信息时。每次HTTP请求都需要携带JWT,可能会增加请求头的大小,尤其是在移动网络环境下,可能会对带宽和流量有较大影响。并且由于JWT自带有效期,当用户登出时,由于JWT无法立即失效,所以只要JWT发放出去,除非在服务器端维护黑名单或使用可撤销的Token机制(如JWT ID Token配合OpenID Connect协议),否则无法立即废弃或更改Token中的权限信息。
JWT应用场景
• 身份验证:JWT通常用于用户登录认证,服务器在验证用户凭据成功后,生成一个带有用户特定信息和过期时间的JWT,客户端收到后,在后续请求中将其作为凭证发送给服务器,服务器仅需验证JWT的签名即可确认用户身份,无需持久化存储会话信息。
• 授权:JWT可以携带权限声明,服务器通过解析JWT中的声明,决定用户是否有权访问特定资源或执行某些操作。
• 单点登录(SSO):由于JWT可以跨域使用,因此在多个子系统间轻松实现SSO功能,用户在一个系统登录后,其JWT可在多个系统间传递,实现无缝登录体验。
在高并发、分布式、跨域及移动端场景下,JWT常因其实现的简洁性和高效性而得到青睐,而在注重数据安全性、复杂会话管理和易控制的场景下,Session可能会是更好的选择。同时,很多现代应用采用了混合策略,结合两者的优势来优化用户体验和系统安全性。
如何使用JWT?
在实际开发中,我们可以使用Spring Boot框架结合JWT来实现用户身份验证。我们来实现一个简单的示例。
我们引入JWT的依赖包:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.4</version>
</dependency>
创建JWT工具类
创建一个JWT工具类,用于生成和验证JWT令牌。这个类通常会包含生成JWT、解析JWT以及提取其中载荷信息的方法。
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
/**
* @version 1.0
* @description: <p></p >
* @author: 码农Academy
* @create: 2024/4/18 14:39
*/
@Component
public class JwtTokenUtil {
@Value("${security.jwt.token.secret-key}")
private String secretKey;
/**过期时间*/
private static final long VALIDITY_IN_MILLISECONDS = 3600000; // 1h
/**
* 生成JWT
* @param authentication 当前登陆用户信息
* @return JWT
*/
public String generateToken(Authentication authentication) {
User user = (User) authentication.getPrincipal();
Date now = new Date();
// 设置过期时间
Date expiryDate = new Date(now.getTime() + VALIDITY_IN_MILLISECONDS);
//指定加密算法
SecureDigestAlgorithm<SecretKey, SecretKey> algorithm = Jwts.SIG.HS256;
//密钥实例
SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes());
return Jwts.builder()
.subject(user.getUsername())
.signWith(key, algorithm) //设置签名使用的签名算法和签名使用的秘钥
.expiration(expiryDate) //设置过期时间
.claim("username", user.getUsername()) //设置自定义负载信息
.compact();
}
/**
* 验证JWT
* @param authToken
* @return
*/
public boolean validateToken(String authToken) {
try {
Jws<Claims> claimsJws = parseJWT(authToken);
claimsJws.getPayload();
return true;
} catch (MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException e) {
return false;
}
}
/**
* 解析jwt
* @param authToken 登陆token
* @return
*/
public Jws<Claims> parseJWT(String authToken){
//密钥实例
SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes());
return Jwts.parser()
.verifyWith(key) //设置签名的密钥
.build()
.parseSignedClaims(authToken); //设置要解析的jwt
}
/**
* 从JWT中获取用户名
* @param token
* @return
*/
public String getUsernameFromToken(String token) {
Jws<Claims> claimsJws = parseJWT(token);
return (String) claimsJws.getPayload().get("username", String.class);
}
}
实现JWT过滤器
创建一个自定义的JWT过滤器,它负责检查每个请求的Authorization header中的Bearer Token,并对其进行验证。验证成功后,创建一个Authentication对象,并将其置于SecurityContext中。
import com.springboot.base.jwt.util.JwtTokenUtil;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;
import java.util.List;
import java.util.stream.Collectors;
/**
* @version 1.0
* @description: <p></p >
* @author: 码农Academy
* @create: 2024/4/18 16:15
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
private static final String BEARER_PREFIX = "Bearer ";
public JwtAuthenticationFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwtToken = getTokenFromRequest(request);
if (jwtToken != null && jwtTokenUtil.validateToken(jwtToken)) {
String username = jwtTokenUtil.getUsernameFromToken(jwtToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 用于权限校验
List<GrantedAuthority> authorities = userDetails.getAuthorities().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, authorities
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}else {
// 如果出现异常,可能是无效的Token,此处可记录日志或做其他处理
System.out.println("没有登陆。。。。。。");
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
return authHeader.replace(BEARER_PREFIX, "");
}
return null;
}
}
配置Spring Security
配置Spring Security以支持JWT身份验证。这通常涉及编写自定义的JWT过滤器,用于拦截请求并在请求头中提取JWT令牌进行验证。过滤器会在验证通过后将用户信息填充到Spring Security的上下文中。
import com.springboot.base.jwt.filter.JwtAuthenticationFilter;
import com.springboot.base.jwt.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @version 1.0
* @description: <p></p >
* @author: 码农Academy
* @create: 2024/4/18 16:11
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests((requests) -> requests
.antMatchers("/user/login").permitAll() // 登录接口公开
.anyRequest().authenticated() // 其他接口需要验证
)
.addFilterBefore(new JwtAuthenticationFilter(userDetailsService, jwtTokenUtil), UsernamePasswordAuthenticationFilter.class) // 添加JWT过滤器
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // 关闭Session管理
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在实现Spring Security的UserDetailsService的接口,用于登陆时查询用户信息,构造Spring Security的User信息。
import com.springboot.base.jwt.login.RoleEntity;
import com.springboot.base.jwt.login.UserEntity;
import com.springboot.base.jwt.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @version 1.0
* @description: <p></p >
* @author: 码农Academy
* @create: 2024/4/18 16:22
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) {
// 根据用户名查询用户实体
UserEntity userEntity = userRepository.findByUserName(username);
// 如果用户不存在,则抛出异常
if (userEntity == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
// 将用户实体转换为Spring Security可识别的UserDetails对象
User user = new User(userEntity.getUserName(),
userEntity.getPassword(), // 注意:此处假设password字段是已经经过加密处理过的
userEntity.getEnabled(),
true, true, true,
getAuthorities(userEntity.getRoleList()) // 获取用户的角色并转换为GrantedAuthority列表
);
System.out.println(user.getPassword());
return user;
}
private Collection<? extends GrantedAuthority> getAuthorities(List<RoleEntity> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleCode()))
.collect(Collectors.toList());
}
}
上述使用Spring Security,如果有疑问的东西可以去查一下Spring Security的工作原理。
登陆接口
我们在定义一个登陆接口,登陆成功后返回jwt生成的token。
import com.springboot.base.jwt.request.LoginRequest;
import com.springboot.base.jwt.response.JwtResponse;
import com.springboot.base.jwt.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @version 1.0
* @description: <p></p >
* @author: 码农Academy
* @create: 2024/4/18 17:31
*/
@RestController
@RequestMapping("/user")
public class LoginController {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(@RequestBody LoginRequest loginRequest) throws Exception {
// 创建基于用户名密码的Authentication请求对象
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
loginRequest.getUserName(),
loginRequest.getPassword()
);
try {
// 使用AuthenticationManager进行身份验证
Authentication authentication = authenticationManager.authenticate(authToken);
// 如果身份验证成功,SecurityContextHolder会存储该Authentication对象
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成JWT Token或其他形式的授权凭证
String token = jwtTokenUtil.generateToken(authentication);
// 返回带有Token的响应
return ResponseEntity.ok(new JwtResponse(token));
} catch (BadCredentialsException ex) {
// 处理用户名/密码不正确等错误情况
System.out.println("密码不正确");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
} catch (DisabledException ex) {
// 处理账户被禁用等情况
System.out.println("处理账户被禁用");
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is disabled");
} catch (AuthenticationException e) {
System.out.println("Authentication failed");
// 其他身份验证异常
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Authentication failed");
}
}
}
import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.Data;
/**
* @version 1.0
* @description: <p></p >
* @author: 码农Academy
* @create: 2024/4/18 18:59
*/
@Data
public class JwtResponse {
private String token;
private String type = "Bearer";
public JwtResponse(String token) {
this.token = token;
}
@JsonCreator
public static JwtResponse of(String token) {
return new JwtResponse(token);
}
}
我们模拟一下登陆发起请求:
登陆成功,返回token。
给一个错误密码,登陆失败:
我们将token设置到Authorization中,发起其他业务请求:
接口请求成功。
我们改变一下token值,在发起请求:
后台打印token校验失败,即未登录。
结论
JWT(JSON Web Tokens)作为一种轻量级且灵活的身份验证和授权机制,在现代web服务及移动应用中得到了广泛应用。它允许服务器端通过加密签名的方式向客户端发放安全的、自包含的令牌,这些令牌可以携带必要的用户身份信息和权限声明,而且由于其无需持久化存储的特性,非常适合于微服务架构下的无状态通信场景。
总结起来,采用JWT进行身份验证具有以下优点:
• 安全性: 通过密钥签名保证令牌的安全性,防止篡改。
• 高效性: 状态less设计减少了服务器端存储负担,提升了系统的可扩展性和响应速度。
• 跨域友好: JWT可轻松应用于多个域名或子系统间的认证需求。
• 自包含性: 令牌自身携带了足够的用户信息,减轻了服务器端查询数据库的频率。
• 有效期可控: 可以在JWT中设置过期时间,从而控制用户的登录会话持续时间。
不过需要注意的是,JWT并非适用于所有场景,尤其是在敏感数据的处理上,因为一旦令牌被截获,黑客们在有效期内可以持续使用。因此,在实施JWT方案时,应充分考虑其适用范围,并配合合适的刷新策略、黑名单机制以及其他安全措施,确保系统的整体安全性和用户体验。