springsecurity-oauth2之认证服务器的编写(二)
这里忽略springcloud-alibaba的搭建,只讲springsecurity的部分。
把生成的密钥对文件放在resources目录下
认证服务器的代码结构图
1.导包
引入spring-security和oauth2的必须包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!--Oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--Oauth2整合jwt-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
2.配置yml
严格来说不需要特别配置,这里只讲两个:
①表示后发现的bean会覆盖之前相同名称的bean
spring:
main:
allow-bean-definition-overriding: true
②配置客户端的clientId、clientSecret(非必须),表示这个客户端的标识,资源服务器会根据这个标识去oauth_client_details找其可访问的资源(即resource_ids字段以及scope字段),实际开发中登录时可由前端(或第三方系统)传来。
auth:
clientId: c1
clientSecret: secret1
3.配置类AuthServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
//对称密钥------改用非对称密钥
//private static final String SIGNING_KEY = "oauth";
@Resource
private AuthenticationManager authenticationManager;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private AuthorizationCodeServices authorizationCodeServices;
@Resource
private JwtAccessTokenConverter accessTokenConverter;
@Resource
private TokenStore tokenStore;
@Resource
private ClientDetailsService clientDetailsService;
/**
* 将客户端信息存储到数据库 ,参照JdbcClientDetailsService源码里数据库建表
* oauth_client_details
* @param dataSource
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
/**
* 配置一个客户端
* 既可以通过授权码获取令牌,也可以通过密码获取令牌(有四种方式)
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
//配置令牌访问端点
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//认证管理(密码模式需要)
.authorizationCodeServices(authorizationCodeServices)//授权码服务
.tokenServices(tokenServices())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许post提交访问令牌
}
/**
* 设置授权码模式的授权码如何存取
*
* @param dataSource
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);
}
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService);//客户端详情
tokenServices.setSupportRefreshToken(true);//支持刷新令牌
tokenServices.setTokenStore(tokenStore);//令牌存储策略
//令牌增强 使用jwt令牌
//使用jwt令牌来替代默认令牌,这样做的好处是携带默认令牌访问资源,每次都要通过授权服务来认证令牌是否有效,
// 而jwt则可以做到资源服务中自己解析从而判断令牌的有效性;另外一个优势就是jwt令牌有更高的安全性,
// 可以使用公钥和私钥进行加密和解密,不容易被破解。
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
tokenServices.setTokenEnhancer(tokenEnhancerChain);
tokenServices.setAccessTokenValiditySeconds(7200);//令牌默认有效期2小时
tokenServices.setRefreshTokenValiditySeconds(9600);//刷新令牌默认有效期
return tokenServices;
}
/**
* 默认是InMemoryTokenStore
* 也可以采用JdbcTokenStore,JwtTokenStore。
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//加载密钥
converter.setKeyPair(keyPair());
return converter;
}
/**
* 读取本地密钥对
* @return
*/
@Bean
public KeyPair keyPair(){
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("macrosoft.jks"),"420188".toCharArray());
KeyPair keyPair = factory.getKeyPair("macrosoft", "420188".toCharArray());
return keyPair;
}
/**
* 令牌的访问策略
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")//oauth/token_key是公开
.checkTokenAccess("permitAll()") //oauth/check_token 已经验证了的客户端才能请求check_token 端点
.allowFormAuthenticationForClients();//表单认证(申请令牌)
}
}
4.配置类SecurityConfig
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()//跨域伪造请求无效
.authorizeRequests().antMatchers("/**").permitAll()//全部放开
.anyRequest().authenticated();
// 登陆设置
http.formLogin();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
}
/**
* 获取新的权限信息
*
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailServiceImpl();
}
/**
* 配置密码的加密规则
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 授权服务需要用到这个bean
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* 忽略拦截url或静态资源文件夹
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.GET,
"/favicon.ico",
"/*.html",
"/**/*.css",
"/**/*.js");
}
/**
* 注入RestTemplate
* @return
*/
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
5.自定义数据库操作UserDetailServiceImpl
public class UserDetailServiceImpl implements UserDetailsService {
@Resource
private SysUserMapper sysUserMapper;
@Resource
private SysRoleMapper sysRoleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails userDetails = null;
try {
//根据username查询用户信息
List<SysUserEntity> sysUserList = sysUserMapper.selectByUserNameSysUsers(username);
if (CollectionUtils.isEmpty(sysUserList)) {
throw new UsernameNotFoundException("该用户不存在!");
}
SysUserEntity sysUserEntity = sysUserList.get(0);
//根据userId查询角色权限
String roles = sysRoleMapper.getRoleNameById(sysUserEntity.getId());
List<GrantedAuthority> authorities = new ArrayList<>();
String[] arrstr = roles.split(";");
if (ObjectUtils.isNotEmpty(arrstr)) {
for (String role : arrstr) {
authorities.add(new SimpleGrantedAuthority(role));
}
}
userDetails =
new User(username
, sysUserEntity.getPassword()
, true
, true
, true
, true
, authorities);
} catch (UsernameNotFoundException e) {
e.printStackTrace();
}
return userDetails;
}
}
6.到这里认证的配置就配完了,这里在放一段登录的代码(service)
@Override
public RespResult<LoginResultDTO> login(LoginRequestDTO loginRequestDTO) {
RespResult<LoginResultDTO> result = new RespResult<>();
ServiceTemplate.executeApiService(getClass(),
AuthScenceEnum.AUTH_LOGIN_SCENCE,
loginRequestDTO,
result,
request -> {
String username = loginRequestDTO.getUsername();
String password = loginRequestDTO.getPassword();
//1.获取当前服务路径
ServiceInstance serviceInstance = loadBalancerClient.choose("macro-auth");
URI uri = serviceInstance.getUri();
String url = uri + "/auth/oauth/token";
//2.组装请求体参数
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("username", username);
body.add("password", password);
//3.在请求头组装clientId和clientSecret
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
//注意:重点,客户端在登录时可以传入clientId、clientSecret(线下给到客户端),那么这样就可以在oauth_client_details配置该clientId的访问资源服务器了
//这里目前写死只有一个clientId、clientSecret(即c1和secret1),在oauth_client_details表中配置了c1对应的resource_ids字段的res1,res2,res3
//也就是说资源服务器标识为res1或res2或res3的,clientId为c1的皆可访问
headers.add("Authorization", getHttpBasic(clientId, clientSecret));
//4.组装最终请求参数
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
//5.发起登录请求接口
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
//6.获取返回数据
Map map = responseEntity.getBody();
boolean flag = (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null);
AssertUtil.assertFalse(flag, AuthErrorEnum.GET_ACCESS_TOKEN_FAIL);
//7.封装返回数据
LoginResultDTO loginResultDTO = new LoginResultDTO();
loginResultDTO.setAccessToken((String) map.get("access_token"));
loginResultDTO.setRefreshToken((String) map.get("refresh_token"));
loginResultDTO.setJti((String) map.get("jti"));
//8.数据存入redis
redisService.setCacheValueForTimes(RedisKeyTemplate.redisUseUserNameAsKey(username), JSON.toJSONString(loginResultDTO),600, TimeUnit.SECONDS);
result.setData(loginResultDTO);
});
return result;
}