前言导读
分析一下为什么要用OAuth2和JWT来做
1. **单点登录(SSO)**方案单击登录方案是最常见的解决方案,但单点登录需要每个与用户交互的服务都必须与认证服务进行通信,这不但会造成重复,**也会产生大量琐碎的网络流量**;
2. **分布式会话(Session)**方案通过将用户会话信息存储在共享存储中,如Redis,并使用用户会话的ID作为key来实现分布式哈希映射。当用户访问微服务时,会话数据就可以从共享存储中获取。该解决方案在高可用和扩展方面都很好,但是由于会话信息保存在共享存储中,所以需要一定的保护机制保护数据安全,因此在具体的实现中会具**有比较高的复杂度**。
3. **客户端令牌(Token)**方案令牌由客户端生成,并由认证服务器签名。在令牌中会包含足够的信息,客户端在请求时会将令牌附加在请求上,从而为各个微服务提供用户身份数据。此方案解决了分布式会话方案的安全性问题,**但如何及时注销用户认证信息则是一个大问题,虽然可以使用短期令牌并频繁地与认证服务器进行校验**,但并不可以彻底解决。JWT(JSON Web Tokens)是非常出名的客户端令牌解决方案,它足够简单,并且对各种环境支持程度也比较高
4. **客户端令牌与API网关结合**
通过在微服务架构中实施API网关,可以将原始的客户端令牌转换为内部会话令牌。一方面可以有效地隐藏微服务,另一方面通过API网关的统一入口可以实现令牌的注销处理。在David Borsos的第二个方案:分布式Session方案中要求开发者能够将用户会话信息单独拎出来进行集中管理。业界比较成熟的开源项目有Spring Session,其使用Redis数据库或缓存机制来实现Session存储,并通过过滤器实现Session数据的自动加载。随着近几年云服务应用的发展,基于令牌(Token)的认证使用范围也越来越广。对于基于令牌认证通常包含下面几层含义:
- 令牌是认证用户信息的集合,而不仅仅是一个无意义的ID。
- 在令牌中已经包含足够多的信息,验证令牌就可以完成用户身份的校验,从而减轻了因为用户验证需要检索数据库的压力,提升了系统性能。
- 因为令牌是需要服务器进行签名发放的,所以如果令牌通过解码认证,我们就可以认为该令牌所包含的信息是合法有效的。
- 服务器会通过HTTP头部中的Authorization获取令牌信息并进行检查,并不需要在服务器端存储任何信息。
- 通过服务器对令牌的检查机制,可以将基于令牌的认证使用在基于浏览器的客户端和移动设备的App或是第三方应用上。
·可以支持跨程序调用。基于Cookie是不允许垮域访问的,而令牌则不存在这个问题。
综上所述,基于令牌的认证由于会包含认证用户的相关信息,因此可以通过验证令牌来完成用户身份的校验,完全不同于之前基于会话的认证。因此,基于令牌的这个优点,像T微信、支付宝、微博及GitHub等,都推出了基于令牌的认证服务,用于访问所开放的API及单点登录。接下来将重点介绍基于令牌认证方案中的OAuth 2.0和JWT
各司其职
OAuth 2.0
根据官网https://oauth.net/2/ OAuth 2.0是用于授权的行业标准协议。OAuth 2.0致力于简化客户端开发人员的工作,同时为Web应用程序,桌面应用程序,移动电话和客厅设备提供特定的授权流程。该规范及其扩展正在IETF OAuth工作组内开发。
范围是OAuth 2.0中的一种机制,用于限制应用程序对用户帐户的访问。一个应用程序可以请求一个或多个范围,然后在同一 屏幕中将此信息呈现给用户,并且颁发给该应用程序的访问令牌将限于所授予的范围。
简单点说就需要两部分:
认证服务端(认证及生成token) 、认证资源服务端(访问其他服务内的资源需要校验)
为什么呢?
在如今的微服务中,一个项目可能有很多的业务服务器,如果业务服务器不受信任的时候,多个服务器之间使用相同的token对用户来说是不安全的,因为任何一个服务器拿到token都可以仿冒用户去另一个服务器处理业务,你的服务器就非常不安全。
怎么做呢?
业务服务器要想得到认证服务器的信任,就只能使用认证服务器的私钥派生的公钥来校验这个业务服务器是否有权限处理。
简单说一下怎么获取公钥
生成一个jks,放在认证服务器中
keytool -genkeypair -alias kevin_key -keyalg RSA -keypass 123456 -keystore kevin_key.jks -storepass 123456
导出公钥
keytool -list -rfc --keystore kevin_key.jks | openssl x509 -inform pem -pubkey
保存文本public_key.txt
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxeI6+R6DsGs5RW21Xu1Fur7iPwGjyngN3SCnwPtdR9jTrQ8EIak+gyjpI/g7gIacHIZKMlVFWoEgjQ7+hIQ5FHBrmSR/S81ezCFjYSjBbdrHYQjMRpn4mEWFmQhIyTRhg1Pb5oTUlWx+L3wc45r6JFdMOlgkKBvfo/7lzwGhxeNp10rfoJcnGDhlfZ3PmoIOYmvg7Z8UwszZpYHWf98164m3hMiPyc81iiy/DEE60OVVepyvynfBwg1aGDyA64w63FZ/2dSwfQ/7VQ7WWJb7oVoIy5pyHslWMuQJPpNCxpOgmb19AgC1GojDSL7WAEq+2gQFrb+7k4PyBdsRYzR9DQIDAQAB
-----END PUBLIC KEY-----
公钥文件需要放在每个业务服务器中
OAuth 2.0客户端授权模式
生产中一般采用授权码模式,简单来说就是你要重定向url认证服务器获取授权码(code),在获取访问令牌。
简单画了一个流程图
在上面流程图中第一步之后,会重定向到类似于
http://localhost:8080/token/oauth/authorize?client_id=client1&response_type=code&redirect_uri=/token
会返回一个code,假设code=2
在访问
http://localhost:8080/oauth/token?client_id=client1&grant_type=authorization_code&redirect_uri=/token&code=2
附加上code值,这时就会返回access_token
还有一个问题:上面url的请求的路径需要保存在数据库中,需要新建一个表,固定的字段,不需要映射实体类,表中的所有字段必须掌握
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL COMMENT '用于唯一标识每一个客户端
(client);注册时必须填写(也可以服务端自动生成),这个字段是必须的,
实际应用也有叫app_key',
`resource_ids` varchar(255) DEFAULT NULL COMMENT '客户端能访问的资
源id集合,注册客户端时,根据实际需要可选择资源id,也可以根据不同的
额注册流程,赋予对应的额资源id',
`client_secret` varchar(255) DEFAULT NULL COMMENT '注册填写或者服务
端自动生成,实际应用也有叫app_secret, 必须要有前缀代表加密方式',
`scope` varchar(255) DEFAULT NULL COMMENT '指定client的权限范围,
比如读写权限,比如移动端还是web端权限',
`authorized_grant_types` varchar(255) DEFAULT NULL COMMENT '可选值
授权码模式:authorization_code,密码模式:password,刷新token: refresh_token, 隐式模式: implicit: 客户端模式: client_credentials。支持多个用逗号分隔\n\n作者:输入昵称就行\n链接:',
`web_server_redirect_uri` varchar(255) DEFAULT NULL COMMENT '客户端
重定向uri,authorization_code和implicit需要该值进行校验,注册时填写,可为空',
`authorities` varchar(255) DEFAULT NULL COMMENT '指定用户的权限范围
,如果授权的过程需要用户登陆,该字段不生效,implicit和
client_credentials需要 可为空',
`access_token_validity` int(11) DEFAULT NULL COMMENT '设置
access_token的有效时间(秒)默认(606012,12小时) 可空',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '设置
refresh_token有效期(秒)默认(606024*30, 30天) 可空',
`additional_information` varchar(4096) DEFAULT NULL COMMENT '
值必须是json格式 可空',
`autoapprove` varchar(255) DEFAULT NULL COMMENT '默认false,适用于
authorization_code模式,设置用户是否自动approval操作,设置true跳过
用户确认授权操作页面,直接跳到redirect_uri',
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这里给出本测试中的某些值,client1代表浏览器访问,client2代表app访问
INSERT INTO `oauth_client_details` VALUES ('client1', 'client1,client2', '{noop}secret1', 'app', 'authorization_code,refresh_token', '/token', NULL, 36000, 36000, NULL, 'true');
INSERT INTO `oauth_client_details` VALUES ('client2', 'client1,client2', '{noop}secret2', 'app', 'authorization_code,refresh_token', '/token', '', 604800, 2592000, '', 'true');
这里还需要数据库保存授权用户,用于校验
CREATE TABLE `sys_user` (
`id` varchar(150) NOT NULL,
`phone` varchar(50) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`password` varchar(150) NOT NULL,
`disable` int(11) NOT NULL,
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`ip` varchar(150) DEFAULT NULL,
PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
JWT
我们发现没有,我们已经获取到了token,为什么还要有jwt呢,这就需要了解他们的生成机制。
token生成的其实就是一个UUID,和业务没有丝毫的关系,这样带来最大的问题,就是需要人工持久化处理token(像处理分布式下的sessionId一样)。但是jwt就不需要,因为自包含,所以token里有身份验证信息,不需要做后台持久化处理,前端每次请求被保护的资源时请求头里带上该token就可以实现。
根据https://jwt.io/introduction/介绍:
JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为JSON对象安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。
尽管可以对JWT进行加密以在双方之间提供保密性,但我们将重点关注已签名的令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌则将这些声明隐藏在其他方的面前。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是对其进行签名的一方。
什么时候应该使用JSON Web令牌?
以下是JSON Web令牌有用的一些情况:
- 授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
- 信息交换:JSON Web令牌是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确定发件人是他们所说的人。另外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
JSON Web令牌结构是什么?
JSON Web令牌以紧凑的形式由三部分组成,这些部分由点(.
)分隔,分别是:
- 标头
- 有效载荷
- 签名
标头
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
有效载荷
令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。索赔有以下三种类型:注册的,公共的和私人索赔。
- 已注册的权利要求:这些是一组非强制性的但建议使用的预定义权利要求,以提供一组有用的,可互操作的权利要求。其中一些是: iss(发出者), exp(到期时间), sub(主题), aud(受众)等。
请注意,声明名称仅是三个字符,因为JWT是紧凑的。 - 公共声明:使用JWT的人可以随意定义这些声明。但是为避免冲突,应在 IANA JSON Web令牌注册表中定义它们,或将其定义为包含抗冲突名称空间的URI。
- 私人权利:这些都是使用它们同意并既不是当事人之间建立共享信息的自定义声明注册或公众的权利要求。
有效负载示例可能是:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
请注意,对于已签名的令牌,此信息尽管可以防止篡改,但任何人都可以读取。除非将其加密,否则请勿将机密信息放入JWT的有效负载或报头元素中。
签名
要创建签名部分,您必须获取编码的标头,编码的有效载荷,机密,标头中指定的算法,并对其进行签名。
例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证消息在整个过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证JWT的发送者是它所说的真实身份。
下图显示了一个JWT,它已对先前的标头和有效负载进行了编码,并用一个秘密进行了签名。
如果您想使用JWT并将这些概念付诸实践,则可以使用http://jwt.io Debugger解码,验证和生成JWT。
JWT使用
之前我们说了,我们需要将UUID的token转换成有用户信息的JWT token,才不需要身份的验证,怎么样去获取UUID的token呢?怎么去获取用户数据呢?
首先讲讲怎么获取用户信息, 在登录时,将请求到
http://172.16.xx.xx:9999/AUTH/auth/login
注意参数是固定格式,不可修改(username,password)deviceType代表是APP还是浏览器自定义
private JSONObject requestToken(String account, String password, String deviceType) {
String result = null;
try {
RestTemplate restTemplate = new RestTemplate();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build();
factory.setHttpClient(httpClient);
restTemplate.setRequestFactory(factory);
MultiValueMap<String, Object> sendMap = new LinkedMultiValueMap<>();
sendMap.add("username", account);
sendMap.add("password", password);
result = RestTemplateUtil.postForEntityFormData(restTemplate, http://172.16.xx.xx:9999/AUTH/auth/login, sendMap, deviceType);
logger.info("认证中心返回结果-------》》》》》" + result);
} catch (Exception e) {
logger.error("error", e);
throw new Exception("500", e);
}
return JSON.parseObject(result);
}
WebSecurityConfigurerAdapter 将会拦截到这个请求,获取到username
根据username获取到用户的详细信息,并通过userDetailsService()检验是否有当前用户的信息,注意这个方法返回了UserDetails,说明UserDetails里有数据了,方便我们获取要增加的userId字段。这里还需要注意:configure方法第一个用于配置用户来源于数据库,第二个配置路径拦截,successHandler重定向url
package org.xx.distributed.auth.conf.auth;
import javax.transaction.Transactional;
import org.bifu.distributed.auth.constant.AuthContants;
import org.bifu.distributed.auth.dao.UserMapper;
import org.bifu.distributed.auth.domain.User;
import org.bifu.distributed.auth.dto.SecurityUserDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
@Autowired
private UserMapper userMapper;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户来源于数据库
// auth.userDetailsService(userDetailsService()).passwordEncoder(new MyPasswordEncoder());
auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
// auth.userDetailsService(userDetailsService()).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
// auth.userDetailsService(userDetailsService()).passwordEncoder(MyPasswordEncoderFactories.createDelegatingPasswordEncoder());
}
/**
* authorizeRequests()配置路径拦截,表明路径访问所对应的权限,角色,认证信息。
* formLogin()对应表单认证相关的配置
* logout()对应了注销相关的配置
* httpBasic()可以配置basic登录
* 配置登陆页/login并允许访问
* 由于使用的是JWT,我们这里不需要csrf
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginProcessingUrl("/auth/login").successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and().csrf().disable()
.sessionManagement()
.maximumSessions(1).expiredUrl("/expiredSession");
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 用户验证
* @return
*/
@Override
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
@Transactional(rollbackOn = Exception.class)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录手机号或邮箱:======"+username);
// 查用户
User user = userMapper.selectByPhoneOrEmail(username, username);
if (user == null) {
throw new UsernameNotFoundException(AuthContants.USER_NOT_EXIST);
}
SecurityUserDTO dto = new SecurityUserDTO();
dto.setId(user.getId());
dto.setUsername(username);
dto.setPassword(user.getPassword());
dto.setDisable(user.getDisable());
// 创建securityUserDTO
// SecurityUserDTO securityUserDTO = new SecurityUserDTO(user);
return dto;
}
};
}
}
这里给出successHandler代码
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.bifu.distributed.auth.constant.AuthContants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final static Logger logger = LoggerFactory.getLogger(MyAuthenticationSuccessHandler.class);
@Value(value = "${prefix.auth}")
private String authPrefix; // /token
@Value(value = "${oauth.redirectUrl}")
private String redirectUrl; // /token
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String deviceType = request.getHeader("deviceType");
logger.info("访问设备-----------》》》" + deviceType);
if (deviceType == null || "".equals(deviceType)) {
deviceType = "browser";
}
// 重定向url到 /token 接口
if ("browser".equals(deviceType)) {
response.sendRedirect("http://localhost:8080:oauth/authorize?client_id=client1&response_type=code&redirect_uri=/token");
} else if ("app".equals(deviceType)) {
response.sendRedirect(http://localhost:8080:oauth/authorize?client_id=client2&response_type=code&redirect_uri=/token);
}
}
}
重定向到这个接口
/**
* 授权,登录
*/
@ResponseBody
@RequestMapping(value = "/token")
public ResultDTO<TokenResultDTO> token(HttpServletRequest request, HttpServletResponse response,
RedirectAttributes attributes) {
logger.info("准备获取token, 获取code = {}", request.getParameter("code"));
TokenResultDTO result = this.userService.token(attributes, request, response);
logger.info("获取到token= {}", JSONObject.toJSONString(result));
return new ResultDTO<TokenResultDTO>(AuthContants.CODE_200, AuthContants.KEYWORD_SUCCESS, result);
}
token方法,这里需要注意一个点,这里返回参数李获取的userId就是UserDetails设置的userId
public TokenResultDTO token(RedirectAttributes attributes, HttpServletRequest request,
HttpServletResponse response) {
try {
String code = request.getParameter("code");
if (StringUtils.isEmpty(code)) {
throw new BusinessException(AuthContants.CODE_EXCEPTION);
}
// 发送请求token
String deviceType = "browser";
if (request.getHeader("deviceType") != null && !"".equals(request.getHeader("deviceType"))) {
deviceType = request.getHeader("deviceType");
}
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> entity = new HttpEntity<String>(headers);
TokenResultDTO tokenResultDTO = null;
if ("browser".equals(deviceType)) {
tokenResultDTO = this.browserRestTemplate.postForObject(
" http://localhost/oauth/token?client_id=client1&grant_type=authorization_code&redirect_uri=/token&code=" + code,
entity, TokenResultDTO.class);
} else if ("app".equals(deviceType)) {
tokenResultDTO = this.appRestTemplate.postForObject(
" http://localhost/oauth/token?client_id=client2&grant_type=authorization_code&redirect_uri=/token&code=" + code,
entity, TokenResultDTO.class);
}
return new TokenResultDTO(tokenResultDTO.getAccess_token(), tokenResultDTO.getRefresh_token(),
tokenResultDTO.getUserId(), tokenResultDTO.getExpires_in());
} catch (BusinessException e) {
logger.error("token?");
throw new Exception("500", e.getMessage());
} catch (Exception e) {
logger.error("token?");
throw new Exception("500", e.getMessage());
}
}
我们获取token之前,然后我们需要增强token,让返回的数据增加一个userId字段
贴一下认证服务器的代码:
package org.xx.distributed.auth.conf.auth;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import com.alibaba.fastjson.JSONObject;
import org.bifu.distributed.auth.constant.AuthContants;
import org.bifu.distributed.auth.dto.SecurityUserDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
/**
* 认证授权服务端
*
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(this.authenticationManager);
endpoints.accessTokenConverter(accessTokenConverter());
endpoints.tokenStore(tokenStore());
endpoints.reuseRefreshTokens(false);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')");
oauthServer.checkTokenAccess("hasAuthorityhasAuthority('ROLE_TRUSTED_CLIENT')");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// jdbc方式 获取配置信息,oauth_client_details给谁发令牌,有效时间、授权模式、刷新令牌等等
clients.withClientDetails(clientDetails());
}
/**
* token converter
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
/***
* 重写增强token方法,用于自定义一些token返回的信息
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
SecurityUserDTO securityUserDTO =
(SecurityUserDTO) authentication.getUserAuthentication().getPrincipal(); //获取当前用户信息
logger.info("重写增强token方法= {}", JSONObject.toJSONString(securityUserDTO));
final Map<String, Object> additionalInformation = new HashMap<>(16);
additionalInformation.put("userId", securityUserDTO.getId());
// 将用户信息添加到token额外信息中
((DefaultOAuth2AccessToken) accessToken)
.setAdditionalInformation(additionalInformation);
OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
return enhancedToken;
}
};
// 非对称加密
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource(AuthContants.JKS_FILE),
AuthContants.JKS_PASSWORD.toCharArray());
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair(AuthContants.JKS_NAME));
return accessTokenConverter;
}
/**
* 定义clientDetails存储的方式-》Jdbc的方式,注入DataSource
*
* @return
*/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* token store
* 返回jwt格式token
* @param
* @return
*/
@Bean
public TokenStore tokenStore() {
TokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
return tokenStore;
}
}
在增强方法中我们看到
additionalInformation.put("userId", securityUserDTO.getId());
已经将userId添加到token额外信息中,但是SecurityUserDTO 是怎么来的呢?
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication)
{
SecurityUserDTO securityUserDTO =
(SecurityUserDTO) authentication.getUserAuthentication().getPrincipal();
可以看到SecurityUserDTO 是通过OAuth2Authentication获取的,但是
OAuth2Authentication怎么获取数据的呢?
贴出部分代码 SecurityUserDTO
public class SecurityUserDTO implements UserDetails {
private static final long serialVersionUID = 1L;
private String id;
private String username;
private String password;
private Integer disable;
private Collection<? extends GrantedAuthority> authorities;
public SecurityUserDTO() {
}
我们进入UserDetails 查看一下源码
/**
* Provides core user information.
*
* <p>
* Implementations are not used directly by Spring Security for security purposes. They
* simply store user information which is later encapsulated into {@link Authentication}
* objects. This allows non-security related user information (such as email addresses,
* telephone numbers etc) to be stored in a convenient location.
* <p>
* Concrete implementations must take particular care to ensure the non-null contract
* detailed for each method is enforced. See
* {@link org.springframework.security.core.userdetails.User} for a reference
* implementation (which you might like to extend or use in your code).
*
* @see UserDetailsService
* @see UserCache
*
/ ** *提供核心用户信息。* * <p> *出于安全目的,Spring Security不会
直接使用实现。它们*仅存储用户信息,这些信息随后封装在{@link
Authentication} *对象中。这允许将与安全无关的用户信息(例如电子邮件
地址,*电话号码等)存储在方便的位置。* <p> *具体实现必须格外小心
,以确保为每个方法强制执行详细的非空合同。请参阅* {@link org.
springframework.security.core.userdetails.User}以获取参考*实现
(您可能希望扩展或在代码中使用)。* * @请参阅UserDetailsService
* @请参阅UserCache *
看到这句话:仅存储用户信息,这些信息随后封装在{@link Authentication} *对象中,我们就明白了吧,其实在前面说到的用户校验中,我们设置了UserDetails
并且它将信息存储到了Authentication中。
public Authentication getUserAuthentication() {
return userAuthentication;
}
就可以获取到用户信息了吧。完美!!!
好了,到这一步我们就实现了JWT对token的增强,那么怎么保证安全性呢?
OAuth2提供了JwtAccessTokenConverter实现,添加jwtSigningKey,以此生成秘钥,以此进行签名
// 非对称加密
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("kevin_key.jks"),
"123456".toCharArray());
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("kevin_key"));
return accessTokenConverter;
到这里基本上就差不多了,贴上一个打印的日志,分析一下整个流程
总结
通过日志可以出
- 用户输入账号、密码,请求/auth/login到认证服务器
- 认证服务器根据username检验用户,存放全局UserDetails
- 认证服务器拦截请求判断是否合法,如果合法进入successHandler进行url重定向
- 重定向获取的code值继续访问/token,这时JWT会对token增加,添加我们需要返回给前段的字段
- 获得access_token和需要的字段
当前代码托管到我的Gitee :
https://gitee.com/ran_song/Limit
欢迎指正交流哦!!
欢迎关注我的微信公众号<搜索:汀雨笔记>,会首发一些最新文章哦!
下面是我的个人网站:http://ransongv587.com
原文链接:
https://mp.weixin.qq.com/s/uLM7XOcLoT08aIVfVjIrkgmp.weixin.qq.com/s/uLM7XOcLoT08aIVfVjIrkg