职贝云数AI新零售门户
标题:
【原创精选】OAuth 2.0+JWT+spring security完成认证授权 ...
[打印本页]
作者:
落叶数思念
时间:
2023-1-18 21:50
标题:
【原创精选】OAuth 2.0+JWT+spring security完成认证授权 ...
(, 下载次数: 0)
上传
点击文件名下载附件
(, 下载次数: 0)
上传
点击文件名下载附件
前言导读
分析一下为什么要用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
复制代码
(, 下载次数: 0)
上传
点击文件名下载附件
导出公钥
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-----
复制代码
公钥文件需求放在每个业务服务器中
(, 下载次数: 0)
上传
点击文件名下载附件
OAuth 2.0客户端授权形式
消费中普通采用
授权码形式
,简单来说就是你要
重定向
url认证服务器获取授权码(code),在获取访问令牌。
简单画了一个流程图
(, 下载次数: 0)
上传
点击文件名下载附件
在下面流程图中第一步之后,会重定向到相似于
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。
(, 下载次数: 0)
上传
点击文件名下载附件
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(&#34;username&#34;, account);
sendMap.add(&#34;password&#34;, password);
result = RestTemplateUtil.postForEntityFormData(restTemplate, http://172.16.xx.xx:9999/AUTH/auth/login, sendMap, deviceType);
logger.info(&#34;认证中心前往结果-------》》》》》&#34; + result);
} catch (Exception e) {
logger.error(&#34;error&#34;, e);
throw new Exception(&#34;500&#34;, 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(&#34;/auth/login&#34;).successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and().csrf().disable()
.sessionManagement()
.maximumSessions(1).expiredUrl(&#34;/expiredSession&#34;);
}
@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(&#34;登录手机号或邮箱:======&#34;+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 = &#34;${prefix.auth}&#34;)
private String authPrefix; // /token
@Value(value = &#34;${oauth.redirectUrl}&#34;)
private String redirectUrl; // /token
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String deviceType = request.getHeader(&#34;deviceType&#34;);
logger.info(&#34;访问设备-----------》》》&#34; + deviceType);
if (deviceType == null || &#34;&#34;.equals(deviceType)) {
deviceType = &#34;browser&#34;;
}
// 重定向url到 /token 接口
if (&#34;browser&#34;.equals(deviceType)) {
response.sendRedirect(&#34;http://localhost:8080:oauth/authorize?client_id=client1&response_type=code&redirect_uri=/token&#34;);
} else if (&#34;app&#34;.equals(deviceType)) {
response.sendRedirect(http://localhost:8080:oauth/authorize?client_id=client2&response_type=code&redirect_uri=/token);
}
}
}
复制代码
重定向到这个接口
/**
* 授权,登录
*/
@ResponseBody
@RequestMapping(value = &#34;/token&#34;)
public ResultDTO<TokenResultDTO> token(HttpServletRequest request, HttpServletResponse response,
RedirectAttributes attributes) {
logger.info(&#34;预备获取token, 获取code = {}&#34;, request.getParameter(&#34;code&#34;));
TokenResultDTO result = this.userService.token(attributes, request, response);
logger.info(&#34;获取到token= {}&#34;, 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(&#34;code&#34;);
if (StringUtils.isEmpty(code)) {
throw new BusinessException(AuthContants.CODE_EXCEPTION);
}
// 发送央求token
String deviceType = &#34;browser&#34;;
if (request.getHeader(&#34;deviceType&#34;) != null && !&#34;&#34;.equals(request.getHeader(&#34;deviceType&#34;))) {
deviceType = request.getHeader(&#34;deviceType&#34;);
}
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> entity = new HttpEntity<String>(headers);
TokenResultDTO tokenResultDTO = null;
if (&#34;browser&#34;.equals(deviceType)) {
tokenResultDTO = this.browserRestTemplate.postForObject(
&#34; http://localhost/oauth/token?client_id=client1&grant_type=authorization_code&redirect_uri=/token&code=&#34; + code,
entity, TokenResultDTO.class);
} else if (&#34;app&#34;.equals(deviceType)) {
tokenResultDTO = this.appRestTemplate.postForObject(
&#34; http://localhost/oauth/token?client_id=client2&grant_type=authorization_code&redirect_uri=/token&code=&#34; + code,
entity, TokenResultDTO.class);
}
return new TokenResultDTO(tokenResultDTO.getAccess_token(), tokenResultDTO.getRefresh_token(),
tokenResultDTO.getUserId(), tokenResultDTO.getExpires_in());
} catch (BusinessException e) {
logger.error(&#34;token?&#34;);
throw new Exception(&#34;500&#34;, e.getMessage());
} catch (Exception e) {
logger.error(&#34;token?&#34;);
throw new Exception(&#34;500&#34;, e.getMessage());
}
}
复制代码
我们获取token之前,然后我们需求加强token,让前往的数据添加一个userId字段
(, 下载次数: 0)
上传
点击文件名下载附件
贴一下认证服务器的代码:
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(&#34;isAnonymous() || hasAuthority(&#39;ROLE_TRUSTED_CLIENT&#39;)&#34;);
oauthServer.checkTokenAccess(&#34;hasAuthorityhasAuthority(&#39;ROLE_TRUSTED_CLIENT&#39;)&#34;);
}
@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(&#34;重写加强token方法= {}&#34;, JSONObject.toJSONString(securityUserDTO));
final Map<String, Object> additionalInformation = new HashMap<>(16);
additionalInformation.put(&#34;userId&#34;, 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(&#34;userId&#34;, 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(&#34;kevin_key.jks&#34;),
&#34;123456&#34;.toCharArray());
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair(&#34;kevin_key&#34;));
return accessTokenConverter;
复制代码
到这里基本上就差不多了,贴上一个打印的日志,分析一下整个流程
(, 下载次数: 0)
上传
点击文件名下载附件
总结
经过日志可以出
用户输入账号、密码,央求/auth/login到认证服务器
认证服务器根据username检验用户,存放全局UserDetails
认证服务器阻拦央求判别能否合法,假如合法进入successHandler停止url重定向
重定向获取的code值继续访问/token,这时JWT会对token添加,添加我们需求前往给前段的字段
获得access_token和需求的字段
当前代码托管到我的Gitee :
https://gitee.com/ran_song/Limit
欢迎指正交流哦!!
欢迎关注我的微信公众号<搜索:汀雨笔记>,会首发一些最新文章哦!
下面是我的个人网站:http://ransongv587.com
原文链接:
作者:
CQPs1KQn
时间:
2023-1-18 21:50
这么专业,真是学习了哈,以后多多指教
作者:
98kqiX
时间:
2023-1-18 21:50
有没有刷新token?
作者:
kM2ugqWV
时间:
2023-1-18 21:51
正在备稿中ing[赞同]
作者:
B5tmA
时间:
2023-1-18 21:51
大佬,gitee上貌似没有这部分代码
作者:
16hxxW4
时间:
2023-1-18 21:51
换到github上了哈,关注下公众号
欢迎光临 职贝云数AI新零售门户 (https://www.taojin168.com/cloud/)
Powered by Discuz! X3.5