Spring-Security-Oauth整合Spring-Security,拦截器
程序的目的主要是,在自己开发的web项目中,即提供前端页面调用访问得接口(带有安全机制),也提供第三方调用的API(基于授权认证的).
在整合的过程中发现SpringSecurity不能到即处理自己的web请求也处理第三方调用请求。所以采用拦截器拦截处理本地的web请求,spring-security-oauth对第三方认证请求进行认证与授权。如果对Oauth2.0不熟悉请参考Oauth2.0介绍,程序主要演示password模式和client模式。
官方样例:
1.pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>springboot</groupId> <artifactId>testSpringBoot</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>18_SpringBoot_codeStandard</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <!-- 继承父包 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> <relativePath></relativePath> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--jdbc --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- Spring Boot Mybatis 依赖 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.2.0</version> </dependency> <!--mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.25</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.2</version> </dependency> <!-- freemarker --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--alibab json --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.44</version> </dependency> <!-- 存放token --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--oauth --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> </dependency> <!--单元测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies> <!--maven的插件 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <executable>true</executable> </configuration> </plugin> </plugins> <pluginManagement> <plugins> <plugin> <!-- 配置java版本 不配置的话默认父类配置的是1.6 --> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <!-- 配置Tomcat插件 --> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> </plugin> </plugins> </pluginManagement> </build> </project>
2.application.properties中增加redis配置
#设置session超时时间 server.session.timeout=2000 spring.redis.host=127.0.0.1 spring.redis.port=6379 #配置oauth2过滤的优先级 security.oauth2.resource.filter-order=3
3.第三方调用API
package com.niugang.controller; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class OauthController { @GetMapping("/api/product/{id}") public String getProduct(@PathVariable String id) { // for debug Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return "product id : " + id; } @GetMapping("/api/order/{id}") public String getOrder(@PathVariable String id) { // for debug Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return "order id : " + id; } }
AuthExceptionEntryPoint.java 自定义token授权失败返回信息
package com.niugang.exception; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 自定义AuthExceptionEntryPoint用于tokan校验失败返回信息 * * @author niugang * */ public class AuthExceptionEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws ServletException { Map<String, Object> map = new HashMap<>(); //401 未授权 map.put("error", "401"); map.put("message", authException.getMessage()); map.put("path", request.getServletPath()); map.put("timestamp", String.valueOf(new Date().getTime())); response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); try { ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(response.getOutputStream(), map); } catch (Exception e) { throw new ServletException(); } } }
CustomAccessDeniedHandler.java 自定义token授权失败返回信息
package com.niugang.exception; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Autowired private ObjectMapper objectMapper; @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); Map<String, Object> map = new HashMap<>(); map.put("error", "403"); map.put("message", accessDeniedException.getMessage()); map.put("path", request.getServletPath()); map.put("timestamp", String.valueOf(new Date().getTime())); response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write(objectMapper.writeValueAsString(map)); } }
以下为password模式通过用户名和密码获取token失败,自定义错误信息。
CustomOauthException.java
package com.niugang.exception; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; /** * * @ClassName: CustomOauthException * @Description:password模式错误处理,自定义登录失败异常信息 * @author: niugang * @date: 2018年9月5日 下午9:44:38 * @Copyright: 863263957@qq.com. All rights reserved. * */ @JsonSerialize(using = CustomOauthExceptionSerializer.class) public class CustomOauthException extends OAuth2Exception { public CustomOauthException(String msg) { super(msg); } }
CustomOauthExceptionSerializer.java
package com.niugang.exception; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Date; import java.util.Map; /** * * @ClassName: CustomOauthExceptionSerializer * @Description:password模式错误处理,自定义登录失败异常信息 * @author: niugang * @date: 2018年9月5日 下午9:45:03 * @Copyright: 863263957@qq.com. All rights reserved. * */ public class CustomOauthExceptionSerializer extends StdSerializer<CustomOauthException> { private static final long serialVersionUID = 1478842053473472921L; public CustomOauthExceptionSerializer() { super(CustomOauthException.class); } @Override public void serialize(CustomOauthException value, JsonGenerator gen, SerializerProvider provider) throws IOException { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); gen.writeStartObject(); gen.writeStringField("error", String.valueOf(value.getHttpErrorCode())); gen.writeStringField("message", value.getMessage()); // gen.writeStringField("message", "用户名或密码错误"); gen.writeStringField("path", request.getServletPath()); gen.writeStringField("timestamp", String.valueOf(new Date().getTime())); if (value.getAdditionalInformation()!=null) { for (Map.Entry<String, String> entry : value.getAdditionalInformation().entrySet()) { String key = entry.getKey(); String add = entry.getValue(); gen.writeStringField(key, add); } } gen.writeEndObject(); } }
CustomWebResponseExceptionTranslator.java
package com.niugang.exception; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; import org.springframework.stereotype.Component; /** * * @ClassName: CustomWebResponseExceptionTranslator * @Description:password模式错误处理,自定义登录失败异常信息 * @author: niugang * @date: 2018年9月5日 下午9:46:36 * @Copyright: 863263957@qq.com. All rights reserved. * */ @Component public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator { @Override public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception { OAuth2Exception oAuth2Exception = (OAuth2Exception) e; return ResponseEntity .status(oAuth2Exception.getHttpErrorCode()) .body(new CustomOauthException(oAuth2Exception.getMessage())); } }
4.配置授权认证服务器
package com.niugang.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; import com.niugang.exception.AuthExceptionEntryPoint; 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.token.store.redis.RedisTokenStore; @Configuration /* * 在当前应用程序上下文中启用授权服务器(即AuthorizationEndpoint和TokenEndpoint)的便利注释, * 它必须是一个DispatcherServlet上下文。服务器的许多特性可以通过使用AuthorizationServerConfigurer类型的@ * bean来定制(例如,通过扩展AuthorizationServerConfigurerAdapter)。用户负责使用正常的Spring安全特性( * @EnableWebSecurity等)来保护授权端点(/oauth/授权),但是令牌端点(/oauth/ * Token)将通过客户端凭证上的HTTP基本身份验证自动获得。 * 客户端必须通过一个或多个AuthorizationServerConfigurers提供一个ClientDetailsService来注册。 */ @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { //模拟第三方调用api private static final String DEMO_RESOURCE_ID = "api"; @Autowired AuthenticationManager authenticationManager; @Autowired RedisConnectionFactory redisConnectionFactory; @Autowired private UserDetailsService userDetailsService; @Autowired private WebResponseExceptionTranslator customWebResponseExceptionTranslator; /**accessTokenValiditySeconds:设置token无效时间,秒 * refreshTokenValiditySeconds:设置refresh_token无效时间秒 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //配置两个客户端,一个用于password认证一个用于client认证 clients.inMemory().withClient("client_1")//基于客户端认证的 .resourceIds(DEMO_RESOURCE_ID) .authorizedGrantTypes("client_credentials", "refresh_token") .scopes("select") .authorities("client") .secret("123456")/*.refreshTokenValiditySeconds(3600).accessTokenValiditySeconds(60)*/ .and().withClient("client_2")//基于密码的 .resourceIds(DEMO_RESOURCE_ID) .authorizedGrantTypes("password", "refresh_token") .scopes("select") .authorities("client") .secret("123456")/*.refreshTokenValiditySeconds(3600).accessTokenValiditySeconds(60)*/; } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(new RedisTokenStore(redisConnectionFactory)) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService);//密码模式需要在数据库中进行认证 endpoints.exceptionTranslator(customWebResponseExceptionTranslator);//错误异常 } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { //允许表单认证 oauthServer.allowFormAuthenticationForClients(); oauthServer.authenticationEntryPoint(new AuthExceptionEntryPoint()); } }
5.配置资源服务器
package com.niugang.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import com.niugang.exception.AuthExceptionEntryPoint; import com.niugang.exception.CustomAccessDeniedHandler; /** * 配置资源服务器 * * @author niugang * */ @Configuration @EnableResourceServer /** * 为OAuth2资源服务器提供方便的注释,使Spring security过滤器能够通过传入的OAuth2令牌验证请求。用户应该添加这个注释, * 并提供一个名为ResourceServerConfigurer的@Bean(例如,通过ResourceServerConfigurerAdapter), * 它指定了资源的详细信息(URL路径和资源id)。为了使用这个过滤器,您必须在您的应用程序中的某个地方使用@EnableWebSecurity, * 或者在您使用这个注释的地方,或者在其他地方。 * * * */ public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter{ private static final String DEMO_RESOURCE_ID = "api"; @Autowired private CustomAccessDeniedHandler customAccessDeniedHandler; @Override public void configure(ResourceServerSecurityConfigurer resources) { //resourceId:指定可访问的资源id //stateless:标记,以指示在这些资源上只允许基于标记的身份验证。 resources.resourceId(DEMO_RESOURCE_ID).stateless(true); resources.authenticationEntryPoint(new AuthExceptionEntryPoint()); resources.accessDeniedHandler(customAccessDeniedHandler); } @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .authorizeRequests() .antMatchers("/api/**").authenticated();//配置api访问控制,必须认证过后才可以访问 } }
6.配置springsecurity
package com.niugang.config; import org.springframework.context.annotation.Configuration; 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; @Configuration @EnableWebSecurity /** * 虽然和oauth认证优先级,起了冲突但是启动也会放置不安全的攻击 * @author niugang * */ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/oauth/**").permitAll(); } }
7.增加拦截器
package com.niugang.interceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; public class LogInterceptor implements HandlerInterceptor { private static Logger logger = LoggerFactory.getLogger(LogInterceptor.class); /** * 执行拦截器之前 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { logger.info("interceptor....在执行前...url:{}", request.getRequestURL()); String user = (String)request.getSession().getAttribute("user"); if(user==null){ response.sendRedirect("/myweb/login"); } return true; //返回false将不会执行了 } /** * 调用完处理器,渲染视图之前 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { logger.info("interceptor.......url:{}", request.getRequestURL()); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
8.配置拦截器
package com.niugang.config; import java.util.concurrent.TimeUnit; import org.springframework.context.annotation.Configuration; import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration @EnableWebMvc public class MvcConfig extends WebMvcConfigurerAdapter { /** * 授权拦截的路径 addPathPatterns:拦截的路径 excludePathPatterns:不拦截的路径 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new com.niugang.interceptor.LogInterceptor()).addPathPatterns("/**").excludePathPatterns("/login/**", "/static/*","/api/**");//"/api/**",不拦截第三方调用的api super.addInterceptors(registry); } /** * 修改springboot中默认的静态文件路径 */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //addResourceHandler请求路径 //addResourceLocations 在项目中的资源路径 //setCacheControl 设置静态资源缓存时间 registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/") .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()); super.addResourceHandlers(registry); } }
9.配置配置springsecurity数据库认证
package com.niugang.service; import java.util.ArrayList; import java.util.List; import javax.annotation.Resource; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import com.niugang.bean.UserQuery; import com.niugang.entity.User; import com.niugang.exception.CheckException; /** * 授权认证业务类 * * @author niugang UserDetailsService spring security包里面的 * 重写loadUserByUsername方法 * */ @Service public class UserDetailsServiceImpl implements UserDetailsService { //UserService自定义的,从数据查询信息 @Resource private UserService userService; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserQuery user = new UserQuery(); user.setName(username); // 查询用户是否存在 List<User> queryList = userService.queryListByPage(user); if (queryList != null & queryList.size() == 1) { // 查询用户拥有的角色 List<GrantedAuthority> list = new ArrayList<GrantedAuthority>(); //如果是admin用户登录,授予SUPERADMIN权限 if(username.equals("admin")){ list.add(new SimpleGrantedAuthority("SUPERADMIN")); } org.springframework.security.core.userdetails.User authUser = new org.springframework.security.core.userdetails.User( queryList.get(0).getName(), queryList.get(0).getPassword(), list); return authUser; } return null; } }
如访问:http:://localhost:8080/myweb/index,没有登录就会跳转到登录页面通过以上配置,对于所有web请求,如果没有登录都会跳转到登录页面,拦截器不会拦截调用api的请求。
访问http:://localhost:8080/myweb/api/order/1会提示没有权限需要认证,默认错误与我们自定义返回信息不一致,并且描述信息较少。那么如何自定义Spring Security Oauth2
异常信息,上面也已经有代码实现
(默认的)
(自定义的)
获取token
进行如上配置之后,启动springboot应用就可以发现多了一些自动创建的endpoints(项目启动的时候也会打印mappings):
{[/oauth/authorize]}
{[/oauth/authorize],methods=[POST]
{[/oauth/token],methods=[GET]}
{[/oauth/token],methods=[POST]}
{[/oauth/check_token]}
{[/oauth/error]
通过单元测试,获取client模式的token
package com.niugang; import java.util.HashMap; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.client.RestTemplate; @RunWith(SpringRunner.class) @SpringBootTest public class Test { @org.junit.Test public void queryToken() { RestTemplate restTemplate = new RestTemplate(); HashMap<String, String> hashMap = new HashMap<>(); hashMap.put("grant_type", "client_credentials"); hashMap.put("scope", "select"); hashMap.put("client_id", "client_1"); hashMap.put("client_secret", "123456"); ResponseEntity<String> postForEntity = restTemplate.postForEntity("http://localhost:8080/myweb/oauth/token?grant_type={grant_type}&scope={scope}&client_id={client_id}&client_secret={client_secret}", String.class, String.class, hashMap); String body = postForEntity.getBody(); System.out.println(body); } }
{"access_token":"5bf8c55d-874d-41fc-94bc-01e2cb8f7142","token_type":"bearer","expires_in":43199,"scope":"select"}
expires_in:访问令牌数秒内的生命周期。例如,值“3600”表示访问令牌将在响应生成后一小时内过期
然后在访问:访问http://localhost:8080/myweb/api/order/1?access_token=bbc81328-69f6-4ff0-8c4c-512f1b8beea3
密码模式也是一样就是放说需要的参数变了
注意此列中的密码模式是基于数据认证的,所以获取token之前确保数据库有对应的username和password
/** * 密码模式 */ @org.junit.Test public void queryToken2() { RestTemplate restTemplate = new RestTemplate(); HashMap<String, String> hashMap = new HashMap<>(); hashMap.put("username", "haha"); hashMap.put("password", "123456"); hashMap.put("grant_type", "password"); hashMap.put("scope", "select"); hashMap.put("client_id", "client_2"); hashMap.put("client_secret", "123456"); ResponseEntity<String> postForEntity = restTemplate.postForEntity( "http://localhost:8080/myweb/oauth/token?username={username}&password= {password}&grant_type={grant_type}&scope={scope}&client_id={client_id}&client_secret= {client_secret}", String.class, String.class, hashMap); String body = postForEntity.getBody(); System.out.println(body); }
{"access_token":"39aa6302-6614-4b94-8553-a96d9ba0f893","token_type":"bearer","refresh_token":"7f2f41dd-4406-4df4-997a-d80178431db8","expires_in":43199,"scope":"select"} //密码模式返回了refresh_token
源码地址:https://gitee.com/niugangxy/springboot
微信公众号