1.Oauth2认证
1.1 Oauth2认证流程
第三方认证技术方案最主要是解决认证协议的通用标准 问题,因为要实现 跨系统认证,各系统之间要遵循一定的接口协议。
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。
参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth协议:https://tools.ietf.org/html/rfc6749
下边分析一个Oauth2认证的例子,第三方网站使用微信认证的过程:
- 客户端请求第三方授权,用户进入第三方应用的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。
- 资源拥有者同意给客户端授权,资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,微信会询问用户是否给第三方网站访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码,并重定向到的网站。
- 客户端获取到授权码,请求认证服务器申请令牌,此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。
- 认证服务器向客户端响应令牌,认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在第三方网站看到已经登录成功。
- 客户端请求资源服务器的资源,第三方网站携带令牌请求访问微信服务器获取用户的基本信息。
- 资源服务器返回受保护资源,资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。
注意:资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务资源服务器通常要请求认证服务器来校验令牌的合法性。
2.探索授权码模式
2.1 概念
上边例举的第三方网站使用微信认证的过程就是授权码模式,流程如下:
- 客户端请求第三方授权
- 用户(资源拥有者)同意给客户端授权
- 客户端获取到授权码,请求认证服务器申请令牌
- 认证服务器向客户端响应令牌
- 客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
- 资源服务器返回受保护资源
2.2 搭建工程
show me your code, talk is cheap.
- 创建 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
//jwt令牌转换器
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
UserDetailsService userDetailsService;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
TokenStore tokenStore;
@Autowired
private CustomUserAuthenticationConverter customUserAuthenticationConverter;
//读取密钥的配置,keytool配置的密钥
@Bean("keyProp")
public KeyProperties keyProperties(){
return new KeyProperties();
}
@Resource(name = "keyProp")
private KeyProperties keyProperties;
//客户端配置,JDBC 方式实现获取已注册的客户端详情
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Override
//配置客户端详情(Client Details)
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(this.dataSource).clients(this.clientDetails());
}
@Bean
@Autowired
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
//使用jwt保存令牌
return new JwtTokenStore(jwtAccessTokenConverter);
}
//使用同一个密钥来编码 JWT 中的 OAuth2 令牌
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory
(keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray())
.getKeyPair(keyProperties.getKeyStore().getAlias(),keyProperties.getKeyStore().getPassword().toCharArray());
converter.setKeyPair(keyPair);
//配置自定义的CustomUserAuthenticationConverter
DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
return converter;
}
//授权服务器端点配置,//告诉Spring Security Token的生成方式
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter)
.authenticationManager(authenticationManager)//认证管理器
.tokenStore(tokenStore)//令牌存储
.userDetailsService(userDetailsService);//用户信息service
}
//授权服务器的安全配置
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients()
.passwordEncoder(new BCryptPasswordEncoder())
//允许所有资源服务器访问公钥端点(/oauth/token_key)
//只允许验证用户访问令牌解析端点(/oauth/check_token)
.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
}
- 创建 CustomUserAuthenticationConverter
@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
@Autowired
UserDetailsService userDetailsService;
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
LinkedHashMap response = new LinkedHashMap();
String name = authentication.getName();
response.put("user_name", name);
Object principal = authentication.getPrincipal();
UserJwt userJwt = null;
if (principal instanceof UserJwt) {
userJwt = (UserJwt) principal;
} else {
//refresh_token默认不去调用userdetailService获取用户信息,这里我们手动去调用,得到 UserJwt
UserDetails userDetails = userDetailsService.loadUserByUsername(name);
userJwt = (UserJwt) userDetails;
}
response.put("name", userJwt.getName());
response.put("id", userJwt.getId());
response.put("utype", userJwt.getUtype());
response.put("userpic", userJwt.getUserpic());
response.put("companyId", userJwt.getCompanyId());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
}
- 创建 WebSecurityConfig
@Configuration
@EnableWebSecurity
@Order(-1)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/userlogin","/userlogout","/userjwt");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
//采用bcrypt对密码进行编码
@Bean
public PasswordEncoder passwordEncoder() {`在这里插入代码片`
return new BCryptPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic().and()
.formLogin()
.and()
.authorizeRequests().anyRequest().authenticated();
}
}
- UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
ClientDetailsService clientDetailsService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//取出身份,如果身份为空说明没有认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
if (authentication == null) {
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
if (clientDetails != null) {
//密码
String clientSecret = clientDetails.getClientSecret();
return new User(username, clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
if (StringUtils.isEmpty(username)) {
return null;
}
FooUserExt userext = new FooUserExt();
userext.setUsername("batman");
userext.setPassword(new BCryptPasswordEncoder().encode("batman"));
userext.setPermissions(new ArrayList<FooMenu>());
if (userext == null) {
return null;
}
//取出正确密码(hash值)
String password = userext.getPassword();
//这里暂时使用静态密码
// String password ="123";
//用户权限,这里暂时使用静态数据,最终会从数据库读取
//从数据库获取权限
List<FooMenu> permissions = userext.getPermissions();
List<String> user_permission = new ArrayList<>();
permissions.forEach(item -> user_permission.add(item.getCode()));
// user_permission.add("course_get_baseinfo");
// user_permission.add("course_find_pic");
String user_permission_string = StringUtils.join(user_permission.toArray(), ",");
UserJwt userDetails = new UserJwt(username,
password,
AuthorityUtils.commaSeparatedStringToAuthorityList(user_permission_string));
userDetails.setId(userext.getId());
userDetails.setUtype(userext.getUtype());//用户类型
userDetails.setCompanyId(userext.getCompanyId());//所属企业
userDetails.setName(userext.getName());//用户名称
userDetails.setUserpic(userext.getUserpic());//用户头像
return userDetails;
}
}
-
之前创建的batman.keystore放在resource目录下(可以参考 https://blog.csdn.net/qq_37362891/article/details/103508579)
-
数据库配置
server:
port: ${PORT:40400}
servlet:
context-path: /auth
spring:
application:
name: security-auth
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
timeout: 5000 #连接超时 毫秒
jedis:
pool:
maxActive: 3
maxIdle: 3
minIdle: 1
maxWait: -1 #连接池最大等行时间 -1没有限制
datasource:
druid:
url: ${MYSQL_URL:jdbc:mysql://localhost:3306/xc_user?characterEncoding=utf-8}
username: root
password: root
driverClassName: com.mysql.cj.jdbc.Driver
initialSize: 5 #初始建立连接数量
minIdle: 5 #最小连接数量
maxActive: 20 #最大连接数量
maxWait: 10000 #获取连接最大等待时间,毫秒
testOnBorrow: true #申请连接时检测连接是否有效
testOnReturn: false #归还连接时检测连接是否有效
timeBetweenEvictionRunsMillis: 60000 #配置间隔检测连接是否有效的时间(单位是毫秒)
minEvictableIdleTimeMillis: 300000 #连接在连接池的最小生存时间(毫秒)
auth:
tokenValiditySeconds: 1200 #token存储到redis的过期时间
clientId: batman
clientSecret: batman
cookieDomain: 127.0.0.1
cookieMaxAge: -1
encrypt:
key-store:
location: classpath:/batman.keystore
secret: batmanstore
alias: batman
password: batman
3.测试授权服务
3.1申请授权码
请求认证服务获取授权码:
Get请求:
localhost:40400/auth/oauth/authorize?
client_id=batman&response_type=code&scop=app&redirect_uri=http://localhost
参数列表如下:
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
首先跳转到登录页面:
输入账号和密码,账号密码都是batman。
点击授权,即可
3.2申请令牌
拿到授权码后,申请令牌。
Post请求:http://localhost:40400/auth/oauth/token
参数如下:
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
此链接需要使用 http Basic认证。
什么是http Basic认证?
http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编码,放在header中请求服务端,一个例子:
Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。
以上测试使用postman完成:
http basic认证:
客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。
Post请求参数:
点击发送:
申请令牌成功:
access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer
Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
到此为止,授权码模式,获取令牌就算完成了。
总结
- 由于学艺不精,只能分享一些操作流程,没有太深入的解析,浅尝辄止。
- 下期预告,我们令牌拿到了,怎么访问服务,请听下篇分享。
如果觉得写得好请给博客一个赞,还有github一个star,谢谢:)。