Spring Boot 鉴权之—— JWT 鉴权
第一:什么是JWT鉴权
1. JWT即JSON Web Tokens,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519),他可以用来安全的传递信息,因为传递的信息是经过加密算法加密过得。
2.JWT常用的加密算法有:HMAC算法或者是RSA的公私秘钥对进行签名,也可以使用公钥/私钥的非对称算法
3.JWT的使用场景主要包括:
1) 认证授权,特别适用于分布式站点的单点登录(SSO)场景,只要用户开放的登录入口登录过一次系统,就会返回一个token,之后的请求都需要包含token。
2)交换信息,通过使用密钥对来安全的传送信息,可以知道发送者是谁、放置消息是否被篡改,一般被用来在身份提供者和服务提供者之间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,例如:设备信息,版本号等,该token也可直接被用于认证,也可被加密。
第二:JWT构成
JSON Web Tokens(JWT)有三部分构成,用英文句点分割(.) ,一般看起来例如:xxxxx.yyyyy.zzzzz
分为:
Header 头信息
Payload 荷载信息,实际数据
Signature 由头信息+荷载信息+密钥 组合之后进行加密得到
1) Header 头信息通常包含两部分,type:代表token的类型,这里使用的是JWT类型。 alg:代表使用的算法,例如HMAC SHA256或RSA.
{
"alg": "HS256",
"typ": "JWT"
} // 这会被经过base64Url编码形成第一部分
2)Payload 一个token的第二部分是荷载信息,它包含一些声明Claim(实体的描述,例:用户信息和其他的一些元数据)
声明分三类:
1)Reserved Claims,这是一套预定义的声明,并不是必须的,这是一套易于使用、操作性强的声明。包括:iss(issuer)、exp(expiration time)、sub(subject)、aud(audience)等
2)Plubic Claims,
3)Private Claims,交换信息的双方自定义的声明
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}//同样经过Base64Url编码后形成第二部分
3) signature 使用header中指定的算法将编码后的header、编码后的payload、一个secret进行加密
例如使用的是HMAC SHA256算法,大致流程类似于: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
这个signature字段被用来确认JWT信息的发送者是谁,并保证信息没有被修改
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
第三步:JWT认证流程
上图是官方提供的一个认证流程图 ,我们可以看到它的授权流程是:
1.客户端通过post请求请求服务端登录认证接口
2.服务端用秘密创建JWT
3.服务端将JWT返回浏览器
4.客户端在授权报头上发送JWT
5.服务端检查JWT签名从JWT获取用户信息
6.服务端向客户端发送响应
通常我们所看到的认证流程,只能看到第一步和第六步,如果使用调试模式或者用抓包工具抓取就可以看到完整流程。
第四步:jwt使用
源码地址:
github: https://github.com/GitHubZhangCom/spring-security-oauth-example/
码云:https://gitee.com/region/spring-security-oauth-example/tree/master/spring-jwt
1)、 引入相关jar:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency> <!-- 使用lombok优雅的编码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
2)、jwt相关编程
JwtUtil:jwt工具类
import org.springframework.util.StringUtils; /** * jwt工具类 * @author zyl * */ public class JwtUtils { private static final String AUTHORIZATION_HEADER_PREFIX = "Bearer "; /** * 获取原始令牌 * remove 'Bearer ' string * * @param authorizationHeader * @return */ public static String getRawToken(String authorizationHeader) { return authorizationHeader.substring(AUTHORIZATION_HEADER_PREFIX.length()); } /** * 获取令牌头 * @param rawToken * @return */ public static String getTokenHeader(String rawToken) { return AUTHORIZATION_HEADER_PREFIX + rawToken; } /** * 验证授权请求头 * @param authorizationHeader * @return */ public static boolean validate(String authorizationHeader) { return StringUtils.hasText(authorizationHeader) && authorizationHeader.startsWith(AUTHORIZATION_HEADER_PREFIX); } /** * 获取授权头前缀 * @return */ public static String getAuthorizationHeaderPrefix() { return AUTHORIZATION_HEADER_PREFIX; } }
JwtAuthenticationFilter
import java.io.IOException; import java.util.ArrayList; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import com.jwt.server.util.JwtUtils; import io.jsonwebtoken.Jwts; /** * 自定义JWT认证过滤器 * 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中, * 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。 * 如果校验通过,就认为这是一个取得授权的合法请求 * @author zyl * */ public class JwtAuthenticationFilter extends BasicAuthenticationFilter { public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith(JwtUtils.getAuthorizationHeaderPrefix())) { chain.doFilter(request, response); return; } UsernamePasswordAuthenticationToken authenticationToken = getUsernamePasswordAuthenticationToken(header); SecurityContextHolder.getContext().setAuthentication(authenticationToken); chain.doFilter(request, response); } private UsernamePasswordAuthenticationToken getUsernamePasswordAuthenticationToken(String token) { String user = Jwts.parser() .setSigningKey("PrivateSecret") .parseClaimsJws(token.replace(JwtUtils.getAuthorizationHeaderPrefix(), "")) .getBody() .getSubject(); if (null != user) { return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>()); } return null; } }
JwtLoginFilter
import java.io.IOException; import java.util.ArrayList; import java.util.Date; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import com.fasterxml.jackson.databind.ObjectMapper; import com.jwt.server.domain.UserInfo; import com.jwt.server.util.JwtUtils; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; /** * 验证用户名密码正确后,生成一个token,并将token返回给客户端 * 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 attemptAuthentication * :接收并解析用户凭证。 successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token。 * */ public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; public JwtLoginFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { UserInfo user = new ObjectMapper().readValue(request.getInputStream(), UserInfo.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>())); } catch (IOException e) { throw new RuntimeException(e); } } // 用户成功登录后,这个方法会被调用,我们在这个方法里生成token @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { String token = Jwts.builder().setSubject(((User) authResult.getPrincipal()).getUsername()) .setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000)) .signWith(SignatureAlgorithm.HS512, "PrivateSecret").compact(); response.addHeader("Authorization", JwtUtils.getTokenHeader(token)); } }
SecurityConfiguration config配置
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import com.jwt.server.filter.JwtAuthenticationFilter; import com.jwt.server.filter.JwtLoginFilter; /** * 通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起 * * @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) 在springboot1.5.8的时候该注解是可以用的 具体看源码 * @author zyl * */ @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity http) throws Exception { //自定义 默认 http.cors().and().csrf().disable().authorizeRequests() .antMatchers("/user/login","/login", "/oauth/authorize").permitAll() .anyRequest().authenticated() .and() .requestMatchers().antMatchers("/user/login","/login","/oauth/authorize") .and() .addFilter(new JwtLoginFilter(authenticationManager()))//登录过滤器 .addFilter(new JwtAuthenticationFilter(authenticationManager()));//自定义过滤器 } }
UserInfo 认证用户
import lombok.Data; /** * 认证用户 * @author zyl * */ @Data public class UserInfo { private String id; private String username; private String password; public UserInfo() { this.setId("testId"); this.setUsername("testUsername"); this.setPassword("testPassword"); }
}
UserDetailServiceImpl:核心认证用户service类
import static java.util.Collections.emptyList; 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 com.jwt.server.domain.UserInfo; /** * * @author zyl * */ @Service public class UserDetailServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { UserInfo user = new UserInfo(); return new User(user.getUsername(), user.getPassword(), emptyList()); } }
第五步:测试
请求登录:localhost:8085/login
这里测试登录的是请用post方式,因为默认源码里边只支持post方式
自定义登录:localhost:8085/user/login