程序的目的主要是,在自己开发的web项目中,即提供前端页面调用访问得接口(带有安全机制),也提供第三方调用的API(基于授权认证的).
在整合的过程中发现SpringSecurity不能到即处理自己的web请求也处理第三方调用请求。所以采用拦截器拦截处理本地的web请求,spring-security-oauth对第三方认证请求进行认证与授权。如果对Oauth2.0不熟悉请参考Oauth2.0介绍,程序主要演示password模式和client模式。
官方样例:
1.pom.xml
4.0.0
springboot
testSpringBoot
0.0.1-SNAPSHOT
jar
18_SpringBoot_codeStandard
http://maven.apache.org
UTF-8
org.springframework.boot
spring-boot-starter-parent
1.5.2.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-jdbc
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.2.0
mysql
mysql-connector-java
com.alibaba
druid
1.0.25
org.apache.commons
commons-lang3
3.2
org.springframework.boot
spring-boot-starter-freemarker
org.springframework.boot
spring-boot-starter-aop
com.alibaba
fastjson
1.2.44
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth
spring-security-oauth2
org.springframework.boot
spring-boot-starter-test
junit
junit
test
org.springframework.boot
spring-boot-maven-plugin
true
maven-compiler-plugin
1.8
1.8
org.apache.tomcat.maven
tomcat7-maven-plugin
2.2
2.application.properties中增加redis配置
#设置session超时时间
server.session.timeout=2000spring.redis.host=127.0.0.1spring.redis.port=6379#配置oauth2过滤的优先级
security.oauth2.resource.filter-order=3
3.第三方调用API
packagecom.niugang.controller;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.context.SecurityContextHolder;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RestController;
@RestControllerpublic classOauthController {
@GetMapping("/api/product/{id}")publicString getProduct(@PathVariable String id) {//for debug
Authentication authentication =SecurityContextHolder.getContext().getAuthentication();return "product id : " +id;
}
@GetMapping("/api/order/{id}")publicString getOrder(@PathVariable String id) {//for debug
Authentication authentication =SecurityContextHolder.getContext().getAuthentication();return "order id : " +id;
}
}
AuthExceptionEntryPoint.java 自定义token授权失败返回信息
packagecom.niugang.exception;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.web.AuthenticationEntryPoint;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.util.Date;importjava.util.HashMap;importjava.util.Map;/*** 自定义AuthExceptionEntryPoint用于tokan校验失败返回信息
*
*@authorniugang
**/
public class AuthExceptionEntryPoint implementsAuthenticationEntryPoint {
@Overridepublic voidcommence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException)throwsServletException {
Map map = new HashMap<>();//401 未授权
map.put("error", "401");
map.put("message", authException.getMessage());
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(newDate().getTime()));
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);try{
ObjectMapper mapper= newObjectMapper();
mapper.writeValue(response.getOutputStream(), map);
}catch(Exception e) {throw newServletException();
}
}
}
CustomAccessDeniedHandler.java 自定义token授权失败返回信息
packagecom.niugang.exception;importjava.io.IOException;importjava.util.Date;importjava.util.HashMap;importjava.util.Map;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.access.AccessDeniedException;importorg.springframework.security.web.access.AccessDeniedHandler;importorg.springframework.stereotype.Component;importcom.fasterxml.jackson.databind.ObjectMapper;
@Componentpublic class CustomAccessDeniedHandler implementsAccessDeniedHandler {
@AutowiredprivateObjectMapper objectMapper;
@Overridepublic voidhandle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException)throwsIOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Map map = new HashMap<>();
map.put("error", "403");
map.put("message", accessDeniedException.getMessage());
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(newDate().getTime()));
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(objectMapper.writeValueAsString(map));
}
}
以下为password模式通过用户名和密码获取token失败,自定义错误信息。
CustomOauthException.java
packagecom.niugang.exception;importcom.fasterxml.jackson.databind.annotation.JsonSerialize;importorg.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 extendsOAuth2Exception {publicCustomOauthException(String msg) {super(msg);
}
}
CustomOauthExceptionSerializer.java
packagecom.niugang.exception;importcom.fasterxml.jackson.core.JsonGenerator;importcom.fasterxml.jackson.databind.SerializerProvider;importcom.fasterxml.jackson.databind.ser.std.StdSerializer;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importjavax.servlet.http.HttpServletRequest;importjava.io.IOException;importjava.util.Date;importjava.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{private static final long serialVersionUID = 1478842053473472921L;publicCustomOauthExceptionSerializer() {super(CustomOauthException.class);
}
@Overridepublic void serialize(CustomOauthException value, JsonGenerator gen, SerializerProvider provider) throwsIOException {
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(newDate().getTime()));if (value.getAdditionalInformation()!=null) {for (Map.Entryentry :
value.getAdditionalInformation().entrySet()) {
String key=entry.getKey();
String add=entry.getValue();
gen.writeStringField(key, add);
}
}
gen.writeEndObject();
}
}
CustomWebResponseExceptionTranslator.java
packagecom.niugang.exception;importorg.springframework.http.ResponseEntity;importorg.springframework.security.oauth2.common.exceptions.OAuth2Exception;importorg.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;importorg.springframework.stereotype.Component;/***
* @ClassName: CustomWebResponseExceptionTranslator
* @Description:password模式错误处理,自定义登录失败异常信息
*@author: niugang
* @date: 2018年9月5日 下午9:46:36
* @Copyright: 863263957@qq.com. All rights reserved.
**/@Componentpublic class CustomWebResponseExceptionTranslator implementsWebResponseExceptionTranslator {
@Overridepublic ResponseEntity translate(Exception e) throwsException {
OAuth2Exception oAuth2Exception=(OAuth2Exception) e;returnResponseEntity
.status(oAuth2Exception.getHttpErrorCode())
.body(newCustomOauthException(oAuth2Exception.getMessage()));
}
}
4.配置授权认证服务器
packagecom.niugang.config;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.core.userdetails.UserDetailsService;importcom.niugang.exception.AuthExceptionEntryPoint;importorg.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;importorg.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;importorg.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;importorg.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;importorg.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;importorg.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@Configuration/** 在当前应用程序上下文中启用授权服务器(即AuthorizationEndpoint和TokenEndpoint)的便利注释,
* 它必须是一个DispatcherServlet上下文。服务器的许多特性可以通过使用AuthorizationServerConfigurer类型的@
* bean来定制(例如,通过扩展AuthorizationServerConfigurerAdapter)。用户负责使用正常的Spring安全特性(
* @EnableWebSecurity等)来保护授权端点(/oauth/授权),但是令牌端点(/oauth/
* Token)将通过客户端凭证上的HTTP基本身份验证自动获得。
* 客户端必须通过一个或多个AuthorizationServerConfigurers提供一个ClientDetailsService来注册。*/@EnableAuthorizationServerpublic class AuthorizationServerConfiguration extendsAuthorizationServerConfigurerAdapter {//模拟第三方调用api
private static final String DEMO_RESOURCE_ID = "api";
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@AutowiredprivateUserDetailsService userDetailsService;
@AutowiredprivateWebResponseExceptionTranslator customWebResponseExceptionTranslator;/**accessTokenValiditySeconds:设置token无效时间,秒
* refreshTokenValiditySeconds:设置refresh_token无效时间秒*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throwsException {//配置两个客户端,一个用于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)*/;
}
@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throwsException {
endpoints
.tokenStore(newRedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);//密码模式需要在数据库中进行认证
endpoints.exceptionTranslator(customWebResponseExceptionTranslator);//错误异常
}
@Overridepublic void configure(AuthorizationServerSecurityConfigurer oauthServer) throwsException {//允许表单认证
oauthServer.allowFormAuthenticationForClients();
oauthServer.authenticationEntryPoint(newAuthExceptionEntryPoint());
}
}
5.配置资源服务器
packagecom.niugang.config;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.http.SessionCreationPolicy;importorg.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;importorg.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;importorg.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;importcom.niugang.exception.AuthExceptionEntryPoint;importcom.niugang.exception.CustomAccessDeniedHandler;/*** 配置资源服务器
*
*@authorniugang
**/@Configuration
@EnableResourceServer/*** 为OAuth2资源服务器提供方便的注释,使Spring security过滤器能够通过传入的OAuth2令牌验证请求。用户应该添加这个注释,
* 并提供一个名为ResourceServerConfigurer的@Bean(例如,通过ResourceServerConfigurerAdapter),
* 它指定了资源的详细信息(URL路径和资源id)。为了使用这个过滤器,您必须在您的应用程序中的某个地方使用@EnableWebSecurity,
* 或者在您使用这个注释的地方,或者在其他地方。
*
*
**/
public class ResourceServerConfiguration extendsResourceServerConfigurerAdapter{private static final String DEMO_RESOURCE_ID = "api";
@AutowiredprivateCustomAccessDeniedHandler customAccessDeniedHandler;
@Overridepublic voidconfigure(ResourceServerSecurityConfigurer resources) {//resourceId:指定可访问的资源id//stateless:标记,以指示在这些资源上只允许基于标记的身份验证。
resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
resources.authenticationEntryPoint(newAuthExceptionEntryPoint());
resources.accessDeniedHandler(customAccessDeniedHandler);
}
@Overridepublic void configure(HttpSecurity http) throwsException {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated();//配置api访问控制,必须认证过后才可以访问
}
}
6.配置springsecurity
packagecom.niugang.config;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity/*** 虽然和oauth认证优先级,起了冲突但是启动也会放置不安全的攻击
*@authorniugang
**/
public class SecurityConfiguration extendsWebSecurityConfigurerAdapter {
@Overrideprotected void configure(HttpSecurity http) throwsException {
http
.authorizeRequests()
.antMatchers("/oauth/**").permitAll();
}
}
7.增加拦截器
packagecom.niugang.interceptor;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.web.servlet.HandlerInterceptor;importorg.springframework.web.servlet.ModelAndView;public class LogInterceptor implementsHandlerInterceptor {private static Logger logger = LoggerFactory.getLogger(LogInterceptor.class);/*** 执行拦截器之前*/@Overridepublic booleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throwsException {
logger.info("interceptor....在执行前...url:{}", request.getRequestURL());
String user= (String)request.getSession().getAttribute("user");if(user==null){
response.sendRedirect("/myweb/login");
}return true; //返回false将不会执行了
}/*** 调用完处理器,渲染视图之前*/@Overridepublic voidpostHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView)throwsException {
logger.info("interceptor.......url:{}", request.getRequestURL());
}
@Overridepublic voidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throwsException {
}
}
8.配置拦截器
packagecom.niugang.config;importjava.util.concurrent.TimeUnit;importorg.springframework.context.annotation.Configuration;importorg.springframework.http.CacheControl;importorg.springframework.web.servlet.config.annotation.EnableWebMvc;importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;importorg.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvcpublic class MvcConfig extendsWebMvcConfigurerAdapter {/*** 授权拦截的路径 addPathPatterns:拦截的路径 excludePathPatterns:不拦截的路径*/@Overridepublic voidaddInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new com.niugang.interceptor.LogInterceptor()).addPathPatterns("/**").excludePathPatterns("/login/**","/static/*","/api/**");//"/api/**",不拦截第三方调用的api
super.addInterceptors(registry);
}/*** 修改springboot中默认的静态文件路径*/@Overridepublic voidaddResourceHandlers(ResourceHandlerRegistry registry) {//addResourceHandler请求路径//addResourceLocations 在项目中的资源路径//setCacheControl 设置静态资源缓存时间
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());super.addResourceHandlers(registry);
}
}
9.配置配置springsecurity数据库认证
packagecom.niugang.service;importjava.util.ArrayList;importjava.util.List;importjavax.annotation.Resource;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.stereotype.Service;importcom.niugang.bean.UserQuery;importcom.niugang.entity.User;importcom.niugang.exception.CheckException;/*** 授权认证业务类
*
*@authorniugang UserDetailsService spring security包里面的
* 重写loadUserByUsername方法
**/@Servicepublic class UserDetailsServiceImpl implementsUserDetailsService {//UserService自定义的,从数据查询信息
@ResourceprivateUserService userService;public UserDetails loadUserByUsername(String username) throwsUsernameNotFoundException {
UserQuery user= newUserQuery();
user.setName(username);//查询用户是否存在
List queryList =userService.queryListByPage(user);if (queryList != null & queryList.size() == 1) {//查询用户拥有的角色
List list = new ArrayList();//如果是admin用户登录,授予SUPERADMIN权限
if(username.equals("admin")){
list.add(new SimpleGrantedAuthority("SUPERADMIN"));
}
org.springframework.security.core.userdetails.User authUser= neworg.springframework.security.core.userdetails.User(
queryList.get(0).getName(), queryList.get(0).getPassword(), list);returnauthUser;
}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
packagecom.niugang;importjava.util.HashMap;importorg.junit.runner.RunWith;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.http.ResponseEntity;importorg.springframework.test.context.junit4.SpringRunner;importorg.springframework.web.client.RestTemplate;
@RunWith(SpringRunner.class)
@SpringBootTestpublic classTest {
@org.junit.Testpublic voidqueryToken() {
RestTemplate restTemplate= newRestTemplate();
HashMap 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 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”表示访问令牌将在响应生成后一小时内过期
密码模式也是一样就是放说需要的参数变了
注意此列中的密码模式是基于数据认证的,所以获取token之前确保数据库有对应的username和password
/*** 密码模式*/@org.junit.Testpublic voidqueryToken2() {
RestTemplate restTemplate= newRestTemplate();
HashMap 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 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
微信公众号