概要
本文内容主要为spring cloud 授权服务的搭建,采用jwt认证。 GitHub 地址:https://github.com/fp2952/spring-cloud-base/tree/master/auth-center/auth-center-provider
添加依赖
Spring Security 及 Security 的OAuth2 扩展
<dependency >
<groupId > org.springframework.cloud</groupId >
<artifactId > spring-cloud-starter-security</artifactId >
</dependency >
<dependency >
<groupId > org.springframework.cloud</groupId >
<artifactId > spring-cloud-starter-oauth2</artifactId >
</dependency >
启动类注解
启动类添加 @EnableAuthorizationServer
注解
@SpringCloudApplication
@EnableAuthorizationServer
@EnableFeignClients ("com.peng.main.client" )
public class AuthCenterProviderApplication {
public static void main (String[] args){
SpringApplication .run (AuthCenterProviderApplication.class, args);
}
}
AuthorizationServerConfigurerAdapter中:
ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。 AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束. AuthorizationServerEndpointsConfigurer:用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。 主要配置如下:
配置客户端详情信息(Client Details)
ClientDetailsServiceConfigurer (AuthorizationServerConfigurer 的一个回调配置项) 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),Spring Security OAuth2的配置方法是编写@Configuration类继承AuthorizationServerConfigurerAdapter,然后重写void configure(ClientDetailsServiceConfigurer clients)方法,如:
@Override
public void configure (ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}
这里使用Jdbc实现客户端详情服务,数据源dataSource不做叙述,使用框架默认的表,schema链接: https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
配置令牌 管理 (jwtAccessTokenConverter)
JwtAccessTokenConverter是用来生成token的转换器,而token令牌默认是有签名的,且资源服务器需要验证这个签名。此处的加密及验签包括两种方式: 对称加密、非对称加密(公钥密钥) 对称加密需要授权服务器和资源服务器存储同一key值,而非对称加密可使用密钥加密,暴露公钥给资源服务器验签,本文中使用非对称加密方式,配置于AuthorizationServerConfigurerAdapter如下:
@Override
public void configure (AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.reuseRefreshTokens(false ).userDetailsService(userDetailsService);
}
<span class="hljs-comment">/**
* 使用非对称加密算法来对Token进行签名
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-meta">@Bean</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> JwtAccessTokenConverter <span class="hljs-title">jwtAccessTokenConverter</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">final</span> JwtAccessTokenConverter converter = <span class="hljs-keyword">new</span> JwtAccessToken();
<span class="hljs-comment">// 导入证书</span>
KeyStoreKeyFactory keyStoreKeyFactory =
<span class="hljs-keyword">new</span> KeyStoreKeyFactory(<span class="hljs-keyword">new</span> ClassPathResource(<span class="hljs-string">"keystore.jks"</span>), <span class="hljs-string">"mypass"</span>.toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair(<span class="hljs-string">"mytest"</span>));
<span class="hljs-keyword">return</span> converter;
}
通过 JDK 工具生成 JKS 证书文件,并将 keystore.jks 放入resource目录下 keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore keystore.jks -storepass mypass
此处我们自定义JwtAccessToken用于添加额外用户信息
public class JwtAccessToken extends JwtAccessTokenConverter {
<span class="hljs-comment">/**
* 生成token
* <span class="hljs-doctag">@param</span> accessToken
* <span class="hljs-doctag">@param</span> authentication
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> OAuth2AccessToken <span class="hljs-title">enhance</span><span class="hljs-params">(OAuth2AccessToken accessToken, OAuth2Authentication authentication)</span> </span>{
DefaultOAuth2AccessToken defaultOAuth2AccessToken = <span class="hljs-keyword">new</span> DefaultOAuth2AccessToken(accessToken);
<span class="hljs-comment">// 设置额外用户信息</span>
BaseUser baseUser = ((BaseUserDetail) authentication.getPrincipal()).getBaseUser();
baseUser.setPassword(<span class="hljs-keyword">null</span>);
<span class="hljs-comment">// 将用户信息添加到token额外信息中</span>
defaultOAuth2AccessToken.getAdditionalInformation().put(Constant.USER_INFO, baseUser);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">super</span>.enhance(defaultOAuth2AccessToken, authentication);
}
<span class="hljs-comment">/**
* 解析token
* <span class="hljs-doctag">@param</span> value
* <span class="hljs-doctag">@param</span> map
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> OAuth2AccessToken <span class="hljs-title">extractAccessToken</span><span class="hljs-params">(String value, Map<String, ?> map)</span></span>{
OAuth2AccessToken oauth2AccessToken = <span class="hljs-keyword">super</span>.extractAccessToken(value, map);
convertData(oauth2AccessToken, oauth2AccessToken.getAdditionalInformation());
<span class="hljs-keyword">return</span> oauth2AccessToken;
}
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">convertData</span><span class="hljs-params">(OAuth2AccessToken accessToken, Map<String, ?> map)</span> </span>{
accessToken.getAdditionalInformation().put(Constant.USER_INFO,convertUserData(map.get(Constant.USER_INFO)));
}
<span class="hljs-function"><span class="hljs-keyword">private</span> BaseUser <span class="hljs-title">convertUserData</span><span class="hljs-params">(Object map)</span> </span>{
String json = JsonUtils.deserializer(map);
BaseUser user = JsonUtils.serializable(json, BaseUser.class);
<span class="hljs-keyword">return</span> user;
}
}
JwtAccessToken 类中从authentication里的getPrincipal(实际为UserDetails接口)获取用户信息,所以我们需要实现自己的UserDetails
public class BaseUserDetail implements UserDetails , CredentialsContainer {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> BaseUser baseUser;
<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> org.springframework.security.core.userdetails.User user;
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">BaseUserDetail</span><span class="hljs-params">(BaseUser baseUser, User user)</span> </span>{
<span class="hljs-keyword">this</span>.baseUser = baseUser;
<span class="hljs-keyword">this</span>.user = user;
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">eraseCredentials</span><span class="hljs-params">()</span> </span>{
user.eraseCredentials();
}
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> Collection<? extends GrantedAuthority> getAuthorities() {
<span class="hljs-keyword">return</span> user.getAuthorities();
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">getPassword</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> user.getPassword();
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">getUsername</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> user.getUsername();
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isAccountNonExpired</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> user.isAccountNonExpired();
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isAccountNonLocked</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> user.isAccountNonLocked();
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isCredentialsNonExpired</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> user.isCredentialsNonExpired();
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isEnabled</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> user.isEnabled();
}
<span class="hljs-function"><span class="hljs-keyword">public</span> BaseUser <span class="hljs-title">getBaseUser</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">return</span> baseUser;
}
}
授权端点开放
@Override
public void configure (AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
.tokenKeyAccess("permitAll()" )
.checkTokenAccess("isAuthenticated()" );
}
Security 配置
需要配置 DaoAuthenticationProvider、UserDetailService 等
@Configuration
@Order (ManagementServerProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
<span class="hljs-comment">// 自动注入UserDetailsService</span>
<span class="hljs-meta">@Autowired</span>
<span class="hljs-keyword">private</span> BaseUserDetailService baseUserDetailService;
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">configure</span><span class="hljs-params">(HttpSecurity http)</span> <span class="hljs-keyword">throws</span> Exception </span>{
http <span class="hljs-comment">// 配置登陆页/login并允许访问</span>
.formLogin().permitAll()
<span class="hljs-comment">// 登出页</span>
.and().logout().logoutUrl(<span class="hljs-string">"/logout"</span>).logoutSuccessUrl(<span class="hljs-string">"/"</span>)
<span class="hljs-comment">// 其余所有请求全部需要鉴权认证</span>
.and().authorizeRequests().anyRequest().authenticated()
<span class="hljs-comment">// 由于使用的是JWT,我们这里不需要csrf</span>
.and().csrf().disable();
}
<span class="hljs-comment">/**
* 用户验证
* <span class="hljs-doctag">@param</span> auth
*/</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">configure</span><span class="hljs-params">(AuthenticationManagerBuilder auth)</span> </span>{
auth.authenticationProvider(daoAuthenticationProvider());
}
<span class="hljs-meta">@Bean</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> DaoAuthenticationProvider <span class="hljs-title">daoAuthenticationProvider</span><span class="hljs-params">()</span></span>{
DaoAuthenticationProvider provider = <span class="hljs-keyword">new</span> DaoAuthenticationProvider();
<span class="hljs-comment">// 设置userDetailsService</span>
provider.setUserDetailsService(baseUserDetailService);
<span class="hljs-comment">// 禁止隐藏用户未找到异常</span>
provider.setHideUserNotFoundExceptions(<span class="hljs-keyword">false</span>);
<span class="hljs-comment">// 使用BCrypt进行密码的hash</span>
provider.setPasswordEncoder(<span class="hljs-keyword">new</span> BCryptPasswordEncoder(<span class="hljs-number">6</span>));
<span class="hljs-keyword">return</span> provider;
}
}
UserDetailsService 实现
@Service
public class BaseUserDetailService implements UserDetailsService {
<span class="hljs-keyword">private</span> Logger logger = LoggerFactory.getLogger(<span class="hljs-keyword">this</span>.getClass());
<span class="hljs-meta">@Autowired</span>
<span class="hljs-keyword">private</span> BaseUserService baseUserService;
<span class="hljs-meta">@Autowired</span>
<span class="hljs-keyword">private</span> BaseRoleService baseRoleService;
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> UserDetails <span class="hljs-title">loadUserByUsername</span><span class="hljs-params">(String username)</span> <span class="hljs-keyword">throws</span> UsernameNotFoundException </span>{
<span class="hljs-comment">// 调用FeignClient查询用户</span>
ResponseData<BaseUser> baseUserResponseData = baseUserService.getUserByUserName(username);
<span class="hljs-keyword">if</span>(baseUserResponseData.getData() == <span class="hljs-keyword">null</span> || !ResponseCode.SUCCESS.getCode().equals(baseUserResponseData.getCode())){
logger.error(<span class="hljs-string">"找不到该用户,用户名:"</span> + username);
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UsernameNotFoundException(<span class="hljs-string">"找不到该用户,用户名:"</span> + username);
}
BaseUser baseUser = baseUserResponseData.getData();
<span class="hljs-comment">// 调用FeignClient查询角色</span>
ResponseData<List<BaseRole>> baseRoleListResponseData = baseRoleService.getRoleByUserId(baseUser.getId());
List<BaseRole> roles;
<span class="hljs-keyword">if</span>(baseRoleListResponseData.getData() == <span class="hljs-keyword">null</span> || !ResponseCode.SUCCESS.getCode().equals(baseRoleListResponseData.getCode())){
logger.error(<span class="hljs-string">"查询角色失败!"</span>);
roles = <span class="hljs-keyword">new</span> ArrayList<>();
}<span class="hljs-keyword">else</span> {
roles = baseRoleListResponseData.getData();
}
<span class="hljs-comment">// 获取用户权限列表</span>
List<GrantedAuthority> authorities = <span class="hljs-keyword">new</span> ArrayList();
roles.forEach(e -> {
<span class="hljs-comment">// 存储用户、角色信息到GrantedAuthority,并放到GrantedAuthority列表</span>
GrantedAuthority authority = <span class="hljs-keyword">new</span> SimpleGrantedAuthority(e.getRoleCode());
authorities.add(authority);
});
<span class="hljs-comment">// 返回带有用户权限信息的User</span>
org.springframework.security.core.userdetails.User user = <span class="hljs-keyword">new</span> org.springframework.security.core.userdetails.User(baseUser.getUserName(),
baseUser.getPassword(), isActive(baseUser.getActive()), <span class="hljs-keyword">true</span>, <span class="hljs-keyword">true</span>, <span class="hljs-keyword">true</span>, authorities);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BaseUserDetail(baseUser, user);
}
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isActive</span><span class="hljs-params">(<span class="hljs-keyword">int</span> active)</span></span>{
<span class="hljs-keyword">return</span> active == <span class="hljs-number">1</span> ? <span class="hljs-keyword">true</span> : <span class="hljs-keyword">false</span>;
}
}
授权服务器验证
http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code&redirect_uri=www.baidu.com
注意:client_id:为存储在数据库里的client_id, response_type:写死code
链接回车后进入spring security 的简单登陆页面,
输入账号密码,为实现的 UserDetailsService 要里获取的用户,点击 login, 进入简单授权页面,点击 Authorize, 重定向到 redirect_uri,并带有 code 参数: http://www.baidu.com?code=rTKETX
post请求获取 token:
注意,此处需加 Authorization
请求头,值为 Basic xxx
xxx 为 client_id:client_secret
的 base64编码。