参考了Springboot2+SpringSecurity+Oauth2+Mysql数据库实现持久化客户端数据
1 基本环境搭建
1.1 数据库脚本
数据库脚本从官方spring-security-oauth中获取,根据你需要创建对应的表.我这里用到了下面几张表。
1.2 ouath2.0相关jar
引入对应的jar,与security和oauth2.0相关的
<!--security oauth2.0配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
1.3 spring security相关配置
import com.dzmsoft.open.oauth.service.UserService;
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.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public PasswordEncoder myPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 匹配下面的路径放过
.antMatchers("/test/01").permitAll()
// 资源必须授权后访问
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()//指定认证页面可以匿名访问
//关闭跨站请求防护
.and()
.httpBasic()
.and()
.csrf().disable();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//UserDetailsService类
auth.userDetailsService(userService)
//加密策略
.passwordEncoder(passwordEncoder);
}
}
1.4 授权服务器配置
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
/**
* 从数据库中查询出客户端信息
* @return
*/
@Bean
public JdbcClientDetailsService clientDetailsService() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
/**
* token保存策略
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
/**
* 授权信息保存策略
* @return
*/
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
/**
* 授权码模式专用对象
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
@Bean
public CustomWebResponseExceptionTranslator myWebResponseExceptionTranslator(){
return new CustomWebResponseExceptionTranslator();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许form表单客户端认证,允许客户端使用client_id和client_secret获取token
security
// .allowFormAuthenticationForClients()
//通过验证返回token信息
// .checkTokenAccess("isAuthenticated()")
.checkTokenAccess("permitAll()")
// 获取token请求不进行拦截
.tokenKeyAccess("permitAll()")
.passwordEncoder(passwordEncoder);
CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security);
endpointFilter.afterPropertiesSet();
endpointFilter.setAuthenticationEntryPoint(customAuthenticationEntryPoint);
// 客户端认证之前的过滤器
security.addTokenEndpointAuthenticationFilter(endpointFilter);
}
/**
* OAuth2的主配置信息
* @return
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore())
.userDetailsService(userDetailsService)
.exceptionTranslator(myWebResponseExceptionTranslator());
}
}
1.5 entrypoint
SpringBoot的Endpoint主要是用来监控应用服务的运行状况,并集成在Mvc中提供查看接口
浅析@ResponseBodyAdvice的理解以及实际应用,ResponseBodyAdvice 接口是在 Controller 执行 return 之后,在 response 返回给客户端之前,执行的对 response 的一些处理,可以实现对 response 数据的一些统一封装或者加密等操作
下面可以看到对返回值的修正。
import com.dzmsoft.open.oauth.dto.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.LinkedHashMap;
@Slf4j
@ControllerAdvice
public class CustomResponseBodyAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
//此处返回true,表示对任何handler的responsebody都调用beforeBodyWrite方法,如果有特殊方法不使用可以考虑使用注解等方式过滤
return true;
}
/**
* 对Controller的所有返回结果进行处理
* @param body 是controller方法中返回的值,对其进行修改后再return
* @param methodParameter
* @param mediaType
* @param aClass
* @param serverHttpRequest
* @param serverHttpResponse
* @return
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
log.info("请求返回数据类型class="+ body.getClass().getName());
if(body instanceof CommonResponse){
return body;
}
CommonResponse<Object> response = new CommonResponse<>();
if (body.toString().contains("error") && body.toString().contains("Unauthorized")){
if(body instanceof LinkedHashMap){
LinkedHashMap<String,String> map = (LinkedHashMap)body;
map.remove("timestamp");
map.remove("status");
map.remove("message");
response.fail(map);
}else{
response.fail(body);
}
}else{
response.success(body);
}
if (log.isDebugEnabled()) {
log.debug("请求返回数据body= " + body.toString());
}
return response;
}
}
异常返回
import com.alibaba.fastjson.JSON;
import com.dzmsoft.open.oauth.dto.CommonResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component("customAuthenticationEntryPoint")
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setStatus(HttpStatus.OK.value());
CommonResponse<Object> response1 = new CommonResponse<>();
response1.fail( String.valueOf(HttpStatus.UNAUTHORIZED.value()),"client_id或client_secret错误",null);
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(response1));
response.getWriter().flush();
}
}
1.6 FrameworkEndpoint
oauth2.0
等请求响应入口在spring-security-oauth2
的jar中,@frameworkendpoint是@Controller的同义词
当然进去之前,先会进入ClientCredentialsTokenEndpointFilter
中
当出现下面的问题,提示无效的身份认证,这又是什么情况呢?
跟踪下去,看到从oauth_client_details
中获取到了用户数据
继续,终于发现是下面的代码显示密码不匹配,调试显示presentedPassword
,userDetails.getPassword()
不匹配,但这两个字符串是相等的,这又是什么原因呢?
这就要追溯到,这个client_id
,client_secret
是如何生成的,数据库中存储的是privateKey
,给到第三方平台的是clientId
和clientSecret
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public static void main(String[] args) {
String uuidString = StringUtil.getUuidString();
String uuidString1 = StringUtil.getUuidString();
String encode = new BCryptPasswordEncoder().encode(uuidString1);
System.out.println("clientId:"+ uuidString);
System.out.println("clientSecret:"+uuidString1);
System.out.println("privateKey:"+encode);
}
5 接口路径授权的问题
过了Oauth2.0这一关,接下来controller层还可能会出现问题,这个异常是DEBUG级别才会出现,所以比较隐蔽
20:05:17.746 DEBUG org.springframework.security.web.FilterChainProxy - Secured POST /error
20:05:17.753 DEBUG o.s.s.w.a.DefaultWebInvocationPrivilegeEvaluator - filter invocation [/error] denied for AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=192.168.3.3, SessionId=6379CB85D657432661A054DE7C951F86], Granted Authorities=[ROLE_ANONYMOUS]]
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73)
at org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator.isAllowed(DefaultWebInvocationPrivilegeEvaluator.java:100)
at org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator.isAllowed(DefaultWebInvocationPrivilegeEvaluator.java:67)
at org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.isAllowed(RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java:76)
at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.isAllowed(ErrorPageSecurityFilter.java:88)
at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.doFilter(ErrorPageSecurityFilter.java:76)
at org.springframework.boot.web.servlet.filter.ErrorPageSecurityFilter.doFilter(ErrorPageSecurityFilter.java:70)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
eptor.java:81)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:122)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:116)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:87)
at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:81)
y.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:149)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:102)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:102)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.doFilter(DefaultLoginPageGeneratingFilter.java:237)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
20:05:17.753 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository - Did not store anonymous SecurityContext
出现该问题是因为路径少配置了,路径被spring security权限纳入管控了