https://www.processon.com/view/link/60a32e7a079129157118740f
微信开发平台文档:
https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Development_Guide.html
(1)令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
(2)令牌可以被数据所有者撤销,会立即失效。密码一般不允许被他人撤销。
(3)令牌有权限范围(scope)。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0 对于如何颁发令牌的细节,规定得非常详细。具体来说,一共分成四种授权类型(authorization grant),即四种颁发令牌的方式,适用于不同的互联网场景。
不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
适用场景:目前市面上主流的第三方验证都是采用这种模式
它的步骤如下:
用户 — JD – 微信
https://b.com/oauth/authorize?response_type=code& #表示授权类型,必选项,此处的值固定为"code"client_id=CLIENT_ID& #表示客户端的ID,必选项redirect_uri=CALLBACK_URL& #表示重定向URI,可选项scope=read& #表示申请的权限范围,可选项state=STATE #表示客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值。
https://a.com/callback?code=AUTHORIZATION_CODE #code参数就是授权码
https://b.com/oauth/token?client_id=CLIENT_ID&client_secret=CLIENT_SECRET& # client_id和client_secret用来让 B 确认 A 的身份,client_secret参数是保密的,因此只能在后端发请求grant_type=authorization_code& # 采用的授权方式是授权码code=AUTHORIZATION_CODE& # 上一步拿到的授权码redirect_uri=CALLBACK_URL # 令牌颁发后的回调网址
{"access_token": "3d80af21-a204-45e9-9bb1-5f9237aad88b", # 令牌"token_type": "bearer","refresh_token": "c016714f-d376-417c-bea1-4d82f37c5b74","expires_in": 3599,"scope": "all"
}
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌,这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)
适用场景:纯前端应用,没有后端
https://b.com/oauth/authorize?response_type=token& # response_type参数为token,表示要求直接返回令牌client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=readhttp://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_uri=http://www.baidu.com
https://a.com/callback#token=ACCESS_TOKEN #token参数就是令牌,A 网站直接在前端拿到令牌。https://www.baidu.com/#access_token=5c9273d1-55ad-4bc8-b928-ec037549a571&token_type=bearer&expires_in=3599
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
适用场景:自家公司搭建的授权服务器
https://oauth.b.com/token?grant_type=password& # 授权方式是"密码式"username=USERNAME&password=PASSWORD&client_id=CLIENT_IDclient_secret=client_secrethttp://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
{access_token: "5c9273d1-55ad-4bc8-b928-ec037549a571",token_type: "bearer",refresh_token: "c016714f-d376-417c-bea1-4d82f37c5b74",expires_in: 3069,scope: "all",
}
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行授权。
适用于没有前端的命令行应用,即在命令行下请求令牌。一般用来提供给我们完全信任的服务器端服务。
https://oauth.b.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECREThttp://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123
{access_token: "c052012f-d260-49b2-b78d-774f44963914",token_type: "bearer",expires_in: 3599,scope: "all",
}
A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了
Header 加 Authorization
curl -H "Authorization: Bearer ACCESS_TOKEN" "https://api.b.com"
也可以通过添加请求参数access_token请求数据
http://localhost:8080/user/getCurrentUser?access_token=3d80af21-a204-45e9-9bb1-5f9237aad88b
令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。
具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
https://b.com/oauth/token?grant_type=refresh_token& # grant_type参数为refresh_token表示要求更新令牌client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN # 用于更新令牌的令牌http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=c016714f-d376-417c-bea1-4d82f37c5b74
{access_token: "ac892fe7-8890-414f-8497-e85c3e6d7e49",token_type: "bearer",refresh_token: "cf749209-3ed7-48cf-a4e3-2e0de33624e8",expires_in: 3599,scope: "all",
}
将OAuth2和Spring Security集成,就可以得到一套完整的安全解决方案。我们可以通过Spring Security OAuth2构建一个授权服务器来验证用户身份以提供access_token,并使用这个access_token来从资源服务器请求数据。
流程:
org.springframework.boot spring-boot-starter-security
org.springframework.security.oauth spring-security-oauth2 2.3.4.RELEASE
或者 引入spring cloud oauth2依赖
org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import org.springframework.cloud spring-cloud-starter-oauth2
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Autowiredprivate UserService userService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {
// super.configure(auth);//获取用户信息auth.userDetailsService(userService);}@Overrideprotected void configure(HttpSecurity http) throws Exception {
// super.configure(http);http.formLogin().permitAll().and().authorizeRequests().antMatchers("/oauth/**").permitAll().antMatchers("/order/**").permitAll().anyRequest().authenticated().and().logout().permitAll().and().csrf().disable();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
UserService
@Service
public class UserService implements UserDetailsService {@Autowired@Lazyprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {String password = passwordEncoder.encode("123456");return new User("mx", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));}
}
UserController
@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/getCurrentUser")public Object getCurrentUser(Authentication authentication) {return authentication.getPrincipal();}
}
资源服务
@Configuration
@EnableResourceServer
public class ResourceServiceConfig extends ResourceServerConfigurerAdapter {@Overridepublic void configure(HttpSecurity http) throws Exception {
// super.configure(http);http.authorizeRequests().anyRequest().authenticated().and().requestMatchers().antMatchers("/user/**");}
}
认证服务
@Configuration // 授权模式 简单模式 密码模式 客户端模式
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate AuthenticationManager authenticationManagerBean;// @Autowired
// private TokenStore redisTokenStore;@Autowiredprivate UserService userService;// 密码模式 刷新令牌@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// super.configure(endpoints);endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
// .tokenStore(redisTokenStore) //指定token存储到redis.reuseRefreshTokens(false) //refresh_token是否重复使用.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); //支持GET,POST请求}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// super.configure(security);//允许表单认证security.allowFormAuthenticationForClients();}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// super.configure(clients);/*** 授权码模式* http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all** implicit: 简化模式* http://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_uri=http://www.baidu.com** password模式* http://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all** 客户端模式* http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123** 刷新令牌* http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]*/clients.inMemory()//配置client_id.withClient("client")//配置client-secret.secret(passwordEncoder.encode("123123"))//配置访问token的有效期.accessTokenValiditySeconds(3600)//配置刷新token的有效期.refreshTokenValiditySeconds(864000)//配置redirect_uri,用于授权成功后跳转.redirectUris("http://www.baidu.com")//配置申请的权限范围.scopes("all")//配置grant_type,表示授权类型/*** 配置grant_type,表示授权类型* authorization_code: 授权码模式* implicit: 简化模式* password: 密码模式* client_credentials: 客户端模式* refresh_token: 更新令牌*/.authorizedGrantTypes("authorization_code","implicit","password","client_credentials","refresh_token");}}
pom.xml
org.springframework.boot spring-boot-starter-data-redis
org.apache.commons commons-pool2
application.yml
spring:redis:host: 127.0.0.1database: 0
config.java
@Configuration
public class RedisConfig {@Autowiredprivate RedisConnectionFactory redisConnectionFactory;@Beanpublic TokenStore tokenStore(){return new RedisTokenStore(redisConnectionFactory);}
}
use
@Autowired
private TokenStore tokenStore;@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置.tokenStore(tokenStore) //指定token存储到redis.reuseRefreshTokens(false) //refresh_token是否重复使用.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求
}
用户只需要登录一次就可以访问权限范围内的所有应用子系统
适用场景:都是企业自己的系统,所有系统都使用同一个一级域名通过不同的二级域名来区分
核心原理:
通过一个单独的授权服务(UAA)来做统一登录,并基于共享UAA的 Cookie 来实现单点登录
核心原理:
pom.xml
org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.2 org.projectlombok lombok
application.yml
server:port: 8080spring:application:name: oauth2-jdbc-demodatasource:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://mysql.localhost.com:3306/oauth2-test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTCusername: rootpassword: roothikari:minimum-idle: 5idle-timeout: 600000maximum-pool-size: 10auto-commit: truepool-name: MyHikariCPmax-lifetime: 1800000connection-timeout: 30000connection-test-query: SELECT 1
WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {
// String password = passwordEncoder().encode("123456");
// auth.inMemoryAuthentication()
// .withUser("admin").password(password).roles("ADMIN")
// .and()
// .withUser("mx").password(password).roles("USER");auth.userDetailsService(userDetailsService());}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/oauth/check_token"); // 将 check_token 暴露出去,否则资源服务器访问时报 403 错误}@Bean@Overrideprotected UserDetailsService userDetailsService() {return new UserServiceImpl();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
AuthorizationServerConfig.java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate DataSource dataSource;@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate AuthenticationManager authenticationManagerBean;@Beanpublic TokenStore tokenStore(){return new JdbcTokenStore(dataSource);}@Beanpublic ClientDetailsService jdbcClientDetailsService(){return new JdbcClientDetailsService(dataSource);//读取oauth_client_details表}// @Autowired
// private PasswordEncoder passwordEncoder;/*** 授权码模式* http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all** implicit: 简化模式* http://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_uri=http://www.baidu.com** password模式* http://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all** 客户端模式* http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123** 刷新令牌* http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// clients.inMemory()
// .withClient("client")
// .secret(passwordEncoder.encode("123123"))
// .authorizedGrantTypes("authorization_code")
// .scopes("app")
// .redirectUris("http://www.baidu.com");clients.withClientDetails(jdbcClientDetailsService());}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置.tokenStore(tokenStore()) //指定token存储到redis.reuseRefreshTokens(false) //refresh_token是否重复使用.userDetailsService(userDetailsService) //刷新令牌授权包含对用户信息的检查.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {//允许表单认证security.allowFormAuthenticationForClients()// 配置校验token需要带入clientId 和clientSeret配置.checkTokenAccess("isAuthenticated()");}
}
application.yml
spring:application:name: oauth2-resource-demodatasource:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://mysql.localhost.com:3306/oauth2-test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTCusername: rootpassword: roothikari:minimum-idle: 5idle-timeout: 600000maximum-pool-size: 10auto-commit: truepool-name: MyHikariCPmax-lifetime: 1800000connection-timeout: 30000connection-test-query: SELECT 1security:oauth2:client:client-id: clientclient-secret: 123123access-token-uri: http://localhost:8080/oauth/tokenuser-authorization-uri: http://localhost:8080/oauth/authorizeresource:token-info-uri: http://localhost:8080/oauth/check_tokenid: ${spring.application.name}server:port: 8088
ResourceServerConfig.java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {@Value("${spring.application.name}")private String appName;@Overridepublic void configure(HttpSecurity http) throws Exception {http.exceptionHandling().and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/contents/").hasAuthority("SystemContent").antMatchers("/contents/view/**").hasAuthority("SystemContentView").antMatchers("/contents/insert/**").hasAuthority("SystemContentInsert").antMatchers("/contents/update/**").hasAuthority("SystemContentUpdate").antMatchers("/contents/delete/**").hasAuthority("SystemContentDelete");}@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {resources.resourceId(appName);super.configure(resources);}
}
server:port: 8082servlet:session:cookie:name: OAUTH2-SSO-CLIENT-DEMO-SESSION-${server.port} #防止Cookie冲突,冲突会导致登录验证不通过security: #与授权服务器对应的配置oauth2:client:client-id: clientclient-secret: 123123user-authorization-uri: http://localhost:8080/oauth/authorizeaccess-token-uri: http://localhost:8080/oauth/tokenresource:token-info-uri: http://localhost:8080/oauth/check_token
@EnableOAuth2Sso
@SpringBootApplication
@EnableOAuth2Sso
public class OAuth2ClientDemoApplication {public static void main(String[] args) {SpringApplication.run(OAuth2ClientDemoApplication.class, args);}
}
网关在认证授权体系里主要负责两件事
微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:
Gateway AuthenticationFilter 认证过滤器
@Component
@Order(0)
public class AuthenticationFilter implements GlobalFilter, InitializingBean {@Autowiredprivate RestTemplate restTemplate;private static Set shouldSkipUrl = new LinkedHashSet<>();@Overridepublic void afterPropertiesSet() throws Exception {// 不拦截认证的请求shouldSkipUrl.add("/oauth/token");shouldSkipUrl.add("/oauth/check_token");shouldSkipUrl.add("/user/getCurrentUser");}@Overridepublic Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {String requestPath = exchange.getRequest().getURI().getPath();//不需要认证的urlif(shouldSkip(requestPath)) {return chain.filter(exchange);}//获取请求头String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");//请求头为空if(StringUtils.isEmpty(authHeader)) {throw new RuntimeException("请求头为空");}TokenInfo tokenInfo=null;try {//获取token信息tokenInfo = getTokenInfo(authHeader);}catch (Exception e) {throw new RuntimeException("校验令牌异常");}// tokenInfoexchange.getAttributes().put("tokenInfo",tokenInfo);return chain.filter(exchange);}private boolean shouldSkip(String reqPath) {for(String skipPath:shouldSkipUrl) {if(reqPath.contains(skipPath)) {return true;}}return false;}private TokenInfo getTokenInfo(String authHeader) {// 往授权服务发请求 /oauth/check_token// 获取token的值String token = StringUtils.substringAfter(authHeader, "bearer ");HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);//必须 basicAuth clienId clientSecretheaders.setBasicAuth(MDA.clientId, MDA.clientSecret);MultiValueMap params = new LinkedMultiValueMap<>();params.add("token", token);HttpEntity> entity = new HttpEntity<>(params, headers);ResponseEntity response = restTemplate.exchange(MDA.checkTokenUrl, HttpMethod.POST, entity, TokenInfo.class);return response.getBody();}
}
Gateway AuthorizationFilter 鉴权过滤器
@Component
@Order(1)
public class AuthorizationFilter implements GlobalFilter, InitializingBean {private static Set shouldSkipUrl = new LinkedHashSet<>();@Overridepublic void afterPropertiesSet() throws Exception {// 不拦截认证的请求shouldSkipUrl.add("/oauth/token");shouldSkipUrl.add("/oauth/check_token");shouldSkipUrl.add("/user/getCurrentUser");}@Overridepublic Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {String requestPath = exchange.getRequest().getURI().getPath();//不需要认证的urlif(shouldSkip(requestPath)) {return chain.filter(exchange);}TokenInfo tokenInfo = exchange.getAttribute("tokenInfo");if(!tokenInfo.isActive()) {throw new RuntimeException("token过期");}hasPremisson(tokenInfo,requestPath);return chain.filter(exchange);}private boolean shouldSkip(String reqPath) {for(String skipPath:shouldSkipUrl) {if(reqPath.contains(skipPath)) {return true;}}return false;}private boolean hasPremisson(TokenInfo tokenInfo,String currentUrl) {boolean hasPremisson = false;//登录用户的权限集合判断List premessionList = Arrays.asList(tokenInfo.getAuthorities());for (String url: premessionList) {if(currentUrl.contains(url)) {hasPremisson = true;break;}}if(!hasPremisson){throw new RuntimeException("没有权限");}return hasPremisson;}
}
辅助类
/*** 常量类*/
public class MDA {public static final String clientId = "gateway-server";public static final String clientSecret = "123123";public static final String checkTokenUrl = "http://oauth2-jdbc-demo/oauth/check_token";}@Data
public class TokenInfo {private boolean active;private String client_id;private String[] scope;private String username;private String[] aud;private Date exp;private String[] authorities;}@Configuration
public class RibbonConfig {@LoadBalanced@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
OAuth 2.0是当前业界标准的授权协议,它的核心是若干个针对不同场景的令牌颁发和管理流程;而JWT是一种轻量级、自包含的令牌,可用于在微服务间安全地传递用户信息
官网: https://jwt.io/
标准: https://tools.ietf.org/html/rfc7519
JWT令牌的优点:
JWT令牌的缺点:
一个JWT实际上就是一个字符串,它由三部分组成,头部(header)、载荷(payload)与签名(signature)
头部用于描述关于该JWT的最基本的信息:类型(即JWT)以及签名所用的算法(如HMACSHA256或RSA)等。
这也可以被表示成一个JSON对象:
{"alg": "HS256","typ": "JWT"
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{"sub": "1234567890","name": "John Doe","iat": 1516239022
}
然后将其进行base64加密,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);var signature = HMACSHA256(encodedString, 'mendd'); // EGSi4DskFrnG-61ydOuB1z5F9ABtJZrfHRFvxVjppkc
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.EGSi4DskFrnG-61ydOuB1z5F9ABtJZrfHRFvxVjppkc
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
如何应用
一般是在请求头里加入Authorization,并加上Bearer标注:
{headers: {'Authorization': 'Bearer ' + token}
}
io.jsonwebtoken jjwt 0.9.1
public class JwtDemoTest {private static final String SECRET_KEY = "123123";public static String testToken() {JwtBuilder jwtBuilder = Jwts.builder().setId("888") //声明的标识{"jti":"888"}.setSubject("MenDD") //主体,用户{"sub":"Mendd"}.setIssuedAt(new Date()) //创建日期{"ita":"xxxxxx"}.setExpiration(new Date(System.currentTimeMillis()+60*1000)) //设置过期时间 1分钟
// .addClaims(map) //直接传入map.claim("roles", "admin").claim("logo", "mendd.jpg").signWith(SignatureAlgorithm.HS256, SECRET_KEY);//签名手段,参数1:算法,参数2:盐String token = jwtBuilder.compact();System.out.println("token: " + token);System.out.println("======parse======");String[] split = token.split("\\.");System.out.println("header: " + Base64Codec.BASE64.decodeToString(split[0]));System.out.println("payload: " + Base64Codec.BASE64.decodeToString(split[1]));//无法解密System.out.println("signature: " + Base64Codec.BASE64.decodeToString(split[2]));return token;}public static void testParseToken(String token){Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();System.out.println("id:"+claims.getId());System.out.println("subject:"+claims.getSubject());System.out.println("issuedAt:"+claims.getIssuedAt());DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));System.out.println("过期时间:"+sf.format(claims.getExpiration()));System.out.println("当前时间:"+sf.format(new Date()));System.out.println("roles:"+claims.get("roles"));System.out.println("logo:"+claims.get("logo"));}public static void main(String[] args) {String token = testToken();System.out.println(token);String extToken = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJNZW5ERCIsImlhdCI6MTY2ODc1MzM2MywiZXhwIjoxNjY4NzUzNDIzLCJyb2xlcyI6ImFkbWluIiwibG9nbyI6Im1lbmRkLmpwZyJ9.F5o7yPI64ZSAI7OVp5BNPSu62u8aHaQmfcBdLhlpONQ";testParseToken(token);testParseToken(extToken);}
}
org.springframework.security spring-security-jwt 1.0.9.RELEASE
io.jsonwebtoken jjwt 0.9.1
@Configuration
public class JwtTokenStoreConfig {@Beanpublic TokenStore jwtTokenStore(){return new JwtTokenStore(jwtAccessTokenConverter());}@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter(){JwtAccessTokenConverter accessTokenConverter = newJwtAccessTokenConverter();//配置JWT使用的秘钥accessTokenConverter.setSigningKey("123123");return accessTokenConverter;}@Beanpublic JwtTokenEnhancer jwtTokenEnhancer() {return new JwtTokenEnhancer();}
}
在授权服务器配置中指定令牌的存储策略为JWT
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig2 extends AuthorizationServerConfigurerAdapter {@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate AuthenticationManager authenticationManagerBean;@Autowiredprivate UserService userService;@Autowired@Qualifier("jwtTokenStore")private TokenStore tokenStore;@Autowiredprivate JwtAccessTokenConverter jwtAccessTokenConverter;@Autowiredprivate JwtTokenEnhancer jwtTokenEnhancer;@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {//配置JWT的内容增强器TokenEnhancerChain enhancerChain = new TokenEnhancerChain();List delegates = new ArrayList<>();delegates.add(jwtTokenEnhancer);delegates.add(jwtAccessTokenConverter);enhancerChain.setTokenEnhancers(delegates);endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置.tokenStore(tokenStore) //配置存储令牌策略.accessTokenConverter(jwtAccessTokenConverter).tokenEnhancer(enhancerChain) //配置tokenEnhancer.reuseRefreshTokens(false) //refresh_token是否重复使用.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {//允许表单认证security.allowFormAuthenticationForClients();}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {/***授权码模式*http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all*http://localhost:8080/oauth/authorize?response_type=code&client_id=client** password模式* http://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all**** 刷新令牌* http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]*/clients.inMemory()//配置client_id.withClient("client")//配置client-secret.secret(passwordEncoder.encode("123123"))//配置访问token的有效期.accessTokenValiditySeconds(3600)//配置刷新token的有效期.refreshTokenValiditySeconds(864000)//配置redirect_uri,用于授权成功后跳转.redirectUris("http://www.baidu.com")//配置申请的权限范围.scopes("all")/*** 配置grant_type,表示授权类型* authorization_code: 授权码* password: 密码* client_credentials: 客户端* refresh_token: 更新令牌*/
.authorizedGrantTypes("authorization_code","password","refresh_token");}
}
JWT内容增强器
public class JwtTokenEnhancer implements TokenEnhancer {@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken,OAuth2Authentication authentication) {Map info = new HashMap<>();info.put("enhance", "enhance info");((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);return accessToken;}
}
使用jjwt工具类来解析Authorization头中存储的JWT内容
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/getCurrentUser")public Object getCurrentUser(Authentication authentication,HttpServletRequest request) {String header = request.getHeader("Authorization");String token = null;if(header!=null){token = header.substring(header.indexOf("bearer") + 7);}else {token = request.getParameter("access_token");}return Jwts.parser().setSigningKey("123123".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();}
}
http://localhost:8080/oauth/token?username=mx&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
{
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBuYW1lIjoiZW5oYW5jZSBpbmZvIGFwcG5hbWUgbWVuZGQiLCJ1c2VyX25hbWUiOiJteCIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2Njg3NTg5NTUsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImU2YzQ5YjJhLTNmODAtNGQ2My1hNTg3LTQ3NDUxZjAzMDEyYiIsImNsaWVudF9pZCI6ImNsaWVudCIsImVuaGFuY2UiOiJlbmhhbmNlIGluZm8ifQ.RfxaL-MB5ibPGWIl7yqlpf0y8e7t6eEYM1YqMA8aCQg",
token_type: "bearer",
refresh_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBuYW1lIjoiZW5oYW5jZSBpbmZvIGFwcG5hbWUgbWVuZGQiLCJ1c2VyX25hbWUiOiJteCIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJlNmM0OWIyYS0zZjgwLTRkNjMtYTU4Ny00NzQ1MWYwMzAxMmIiLCJleHAiOjE2Njk2MTkzNTUsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjFhNDc3NTc2LTAwOTItNDYwYy1hM2RlLWIyYzk3ODAyMzMxZCIsImNsaWVudF9pZCI6ImNsaWVudCIsImVuaGFuY2UiOiJlbmhhbmNlIGluZm8ifQ.Yk8J4tf49WrfRbd6yZrc8WqyNIL98XTygiI9tzVhxCA",
expires_in: 3599,
scope: "all",
appname: "enhance info appname mendd",
enhance: "enhance info",
jti: "e6c49b2a-3f80-4d63-a587-47451f03012b",
}
curl --location --request GET 'http://localhost:8080/user/getCurrentUser' \
--header 'Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBuYW1lIjoiZW5oYW5jZSBpbmZvIGFwcG5hbWUgbWVuZGQiLCJ1c2VyX25hbWUiOiJteCIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2Njg3NTg5NTUsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImU2YzQ5YjJhLTNmODAtNGQ2My1hNTg3LTQ3NDUxZjAzMDEyYiIsImNsaWVudF9pZCI6ImNsaWVudCIsImVuaGFuY2UiOiJlbmhhbmNlIGluZm8ifQ.RfxaL-MB5ibPGWIl7yqlpf0y8e7t6eEYM1YqMA8aCQg'
http://localhost:8080/user/getCurrentUser?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBuYW1lIjoiZW5oYW5jZSBpbmZvIGFwcG5hbWUgbWVuZGQiLCJ1c2VyX25hbWUiOiJteCIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2Njg3NTg5NTUsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImU2YzQ5YjJhLTNmODAtNGQ2My1hNTg3LTQ3NDUxZjAzMDEyYiIsImNsaWVudF9pZCI6ImNsaWVudCIsImVuaGFuY2UiOiJlbmhhbmNlIGluZm8ifQ.RfxaL-MB5ibPGWIl7yqlpf0y8e7t6eEYM1YqMA8aCQg
{appname: "enhance info appname mendd",user_name: "mx",scope: ["all"],exp: 1668758955,authorities: ["admin"],jti: "e6c49b2a-3f80-4d63-a587-47451f03012b",client_id: "client",enhance: "enhance info",
}
上一篇:Nodejs编写接口