SpringBoot教程(三)——集成Spring Security OAuth2 JWT
前言关于Oauth2的名词概念说明和可以移步理解OAuth 2.0 - 阮一峰,这是一篇对于oauth2很好的科普文章。本文主要实现SpeingBoot2.x+Outh2+JWT对微服务进行认证授权、权限校验。本文仅涉及password模式,因为公司项目不涉及第三方登录,其他模式逻辑相似,可自行完善。源码已上传github,可留言获取。一、核心依赖<!-- spring security -
·
前言
关于OAuth2的名词概念说明和可以移步理解OAuth 2.0 - 阮一峰,这是一篇对于OAuth2很好的科普文章。本文主要实现SpeingBoot2.x+OAuth2+JWT对微服务进行认证授权、权限校验。本文仅涉及password模式,因为公司项目不涉及第三方登录,其他模式逻辑相似,可自行完善。
一、核心依赖
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring security oauth2 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
<!-- spring security jwt -->
<dependency>
groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
二、认证授权服务器
1.认证服务核心配置
/**
* @Auther: zlx
* @Date: 2020/4/18 16:04
* @Description: 授权服务配置
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private JwtTokenStore jwtTokenStore;
@Autowired
private AuthenticationManager authenticationManager;
@Qualifier("jwtAccessTokenConverter")
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private PasswordEncoder passwordEncoder;
//1.客户端详情服务
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(CustomConstants.OAUTH2_CLIENT_ID)// 客户端id
.secret(passwordEncoder.encode(CustomConstants.OAUTH2_CLIENT_SECRET))//客户端密钥
.resourceIds(CustomConstants.OAUTH2_CLIENT_RESOURCE_ID)//资源列表
.authorizedGrantTypes(CustomConstants.OAUTH2_GRANT_TYPE)// 该client允许的授权类型password
.scopes(CustomConstants.OAUTH2_SCOPE)// 允许的授权范围
.autoApprove(true);//true表示直接发放令牌
}
//2.配置令牌访问端点
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)//密码模式需要配置
.tokenServices(tokenService())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET);//请求令牌运行post方式
// 自定义异常转换类
endpoints.exceptionTranslator(new CustomWebResponseExceptionTranslator());
}
//3.配置令牌访问端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
//注入自定义过滤器
String path = CustomConstants.OAUTH2_DEFAULT_TOKEN_PATH;
CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security, path);
endpointFilter.afterPropertiesSet();
endpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint());
security.addTokenEndpointAuthenticationFilter(endpointFilter);
security.authenticationEntryPoint(authenticationEntryPoint());
security
.tokenKeyAccess("permitAll()") //oauth/token_key是公开
.checkTokenAccess("isAuthenticated()"); //oauth/check_token需要登录
}
//令牌管理服务
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setSupportRefreshToken(false);//支持刷新令牌
service.setReuseRefreshToken(false);//是否复用refresh_token,默认为true(如果为false,则每次请求刷新都会删除旧的refresh_token,创建新的refresh_token)
service.setTokenStore(jwtTokenStore);//令牌存储策略
service.setAccessTokenValiditySeconds(CustomConstants.OAUTH2_TOKEN_VALIDITY_SECONDS); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(CustomConstants.OAUTH2_REFRESH_TOKEN_VALIDITY_SECONDS); // 刷新令牌默认有效期3天
//令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(jwtAccessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
return service;
}
//在认证服务器注入异常处理逻辑,自定义异常返回结果
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, e) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setStatus(HttpStatus.OK.value());
response.setHeader("Content-Type", "application/json;charset=UTF-8");
response.getWriter().write(JSONUtil.toJsonPrettyStr(Result.failure(ResultStatus.CLIENT_AUTHENTICATION_FAILED.getCode(), ResultStatus.CLIENT_AUTHENTICATION_FAILED.getMessage())));
};
}
}
2.安全服务配置
/**
* @Auther: zlx
* @Date: 2020/4/18 16:04
* @Description: 安全服务配置
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailServiceImpl userDetailService;
//认证管理器 密码模式必须配
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
return daoAuthenticationProvider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(daoAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().csrf().disable()
.authorizeRequests().antMatchers("/oauth/**").permitAll()
.and()
.authorizeRequests().anyRequest().authenticated();
}
}
3.配置token生成方案,使用JWT生成令牌
/**
* @Auther: zlx
* @Date: 2020/4/18 16:28
* @Description: token配置
*/
@Configuration
public class TokenConfig {
//JWT令牌存储方案
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(CustomConstants.OAUTH2_JWT_SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
}
4.重写客户端认证过滤器,自定义异常处理,不使用默认的 OAuth2AuthenticationEntryPoint处理异常
/**
* @Auther: zlx
* @Date: 2021/3/8 13:09
* @Description: 重写客户端认证过滤器,不使用默认的 OAuth2AuthenticationEntryPoint处理异常
*/
public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {
private final AuthorizationServerSecurityConfigurer configurer;
private AuthenticationEntryPoint authenticationEntryPoint;
public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer, String path) {
this.configurer = configurer;
setFilterProcessesUrl(path);
}
@Override
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
super.setAuthenticationEntryPoint(null);
this.authenticationEntryPoint = authenticationEntryPoint;
}
@Override
protected AuthenticationManager getAuthenticationManager() {
return configurer.and().getSharedObject(AuthenticationManager.class);
}
@Override
public void afterPropertiesSet() {
setAuthenticationFailureHandler((request, response, e) -> authenticationEntryPoint.commence(request, response, e));
setAuthenticationSuccessHandler((request, response, authentication) -> {
});
}
}
5.自定义异常响应格式
/**
* @Auther: zlx
* @Date: 2021/3/5 20:52
* @Description: 自定义异常类
*/
@Slf4j
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
@Override
public ResponseEntity<Result<Void>> translate(Exception e) {
Result<Void> response = resolveException(e);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getCode()));
}
/**
* 构建返回异常
*/
private Result<Void> resolveException(Exception e) {
// 初始值 500
ResultStatus resultStatus = ResultStatus.ERROR;
//不支持的认证方式
if (e instanceof UnsupportedGrantTypeException) {
resultStatus = ResultStatus.UNSUPPORTED_GRANT_TYPE;
//用户名或密码异常
} else if (e instanceof InvalidGrantException || e instanceof InternalAuthenticationServiceException) {
resultStatus = ResultStatus.USERNAME_OR_PASSWORD_ERROR;
} else if (e instanceof DisabledException) {
resultStatus = ResultStatus.DISABLED_EXCEPTION;
} else if (e instanceof LockedException) {
resultStatus = ResultStatus.LOCKED_EXCEPTION;
} else if (e instanceof AccountExpiredException) {
resultStatus = ResultStatus.ACCOUNT_EXPIRED_EXCEPTION;
} else if (e instanceof CredentialsExpiredException) {
resultStatus = ResultStatus.CREDENTIALS_EXPIRED_EXCEPTION;
}
return Result.failure(resultStatus.getCode(), resultStatus.getMessage());
}
}
6.重写用户认证信息,实现UserDetailsService接口
/**
* @Auther: zlx
* @Date: 2020/4/18 16:04
* @Description: 查询用户信息
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Autowired
private SysMenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userMapper.selectOne(new QueryWrapper<SysUser>().eq("username", username));
if (sysUser == null) {
throw new CustomException("用户不存在");
}
//获取所有角色
List<String> permissionList = menuMapper.selectMenuUrlByUserId(sysUser.getId());
if (CollectionUtils.isEmpty(permissionList)) {
throw new CustomException("该用户未分配权限");
}
Oauth2User oauth2User = new Oauth2User();
BeanUtils.copyProperties(sysUser, oauth2User);
oauth2User.setEnabled(sysUser.getStatus() == 0);
oauth2User.setAuthorities(AuthorityUtils.createAuthorityList(permissionList.toArray(new String[0])));
if (!oauth2User.isEnabled()) {
throw new DisabledException("该账户已被禁用!");
} else if (!oauth2User.isAccountNonLocked()) {
throw new LockedException("该账号已被锁定!");
} else if (!oauth2User.isAccountNonExpired()) {
throw new AccountExpiredException("该账号已过期!");
} else if (!oauth2User.isCredentialsNonExpired()) {
throw new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录!");
}
return oauth2User;
}
}
三、资源服务器
1.资源服务器核心配置
/**
* @Auther: zlx
* @Date: 2020/4/18 18:31
* @Description: 资源服务器
*/
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private JwtTokenStore tokenStore;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
private AuthExceptionEntryPoint authExceptionEntryPoint;
@Autowired
private OAuth2WebSecurityExpressionHandler expressionHandler;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(CustomConstants.OAUTH2_CLIENT_RESOURCE_ID)//认证服务器配置的资源id
.tokenStore(tokenStore)//jwt验证token
.accessDeniedHandler(customAccessDeniedHandler)//自定义权限校验异常
.authenticationEntryPoint(authExceptionEntryPoint)//自定义token校验异常
.stateless(true).expressionHandler(expressionHandler);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/oauth/**", CustomConstants.OAUTH2_CUSTOM_TOKEN_PATH).permitAll()//放行接口
.and()
.authorizeRequests().antMatchers("/user/getUserInfo").authenticated()//只要认证通过就可以访问的接口
.and()
.authorizeRequests().antMatchers("/**").access("@security.hasPermission(authentication, request)");//其他所有接口走自定义权限判断
}
@Bean
public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {
OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
}
2.自定义权限判断
@Component("security")
public class SecurityService {
/**
* @return 返回true放行 false会被自定义的权限校验异常处理
* {@link com.equipment.life.cycle.oauth2.resource.expection.CustomAccessDeniedHandler}
*/
public boolean hasPermission(Authentication authentication, HttpServletRequest request) {
String requestURI = request.getRequestURI();//当前请求的URL
//判断当前请求的URL是否在用户所拥有的权限中
return authentication
.getAuthorities()
.stream()
.anyMatch((u) -> u.getAuthority().equals(requestURI));
}
}
3.自定义token校验失败异常返回格式
@Component
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {
@SneakyThrows
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
response.setStatus(HttpStatus.OK.value());
response.setHeader("Content-Type", "application/json;charset=UTF-8");
System.out.println(authException.getMessage());
try {
response.getWriter().write(JSONUtil.toJsonPrettyStr(Result.failure(ResultStatus.TOKEN_VERIFICATION_FAILED)));
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.自定义权限校验异常返回格式
@Component("customAccessDeniedHandler")
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
response.setStatus(HttpStatus.OK.value());
response.setHeader("Content-Type", "application/json;charset=UTF-8");
try {
response.getWriter().write(JSONUtil.toJsonPrettyStr(Result.failure(ResultStatus.FULL_AUTHENTICATION)));
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、测试效果图
(1)获取token成功
(2)获取token失败
(3)获取用户信息
(4)请求资源接口成功
(5)请求资源接口失败
更多推荐
已为社区贡献1条内容
所有评论(0)