程序的目的主要是,在自己开发的web项目中,即提供前端页面调用访问得接口(带有安全机制),也提供第三方调用的API(基于授权认证的).
在整合的过程中发现SpringSecurity不能到即处理自己的web请求也处理第三方调用请求。所以采用拦截器拦截处理本地的web请求,spring-security-oauth对第三方认证请求进行认证与授权。如果对Oauth2.0不熟悉请参考Oauth2.0介绍,程序主要演示password模式和client模式。
官方样例:
https://github.com/spring-projects/spring-security-oauth/blob/master/samples/oauth2/sparklr/src/main/java/org/springframework/security/oauth/examples/sparklr/config/OAuth2ServerConfig.java
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=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 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 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: [email protected]. 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: [email protected]. All rights reserved.
*
*/
public class CustomOauthExceptionSerializer extends StdSerializer {
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 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: [email protected]. All rights reserved.
*
*/
@Component
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
@Override
public ResponseEntity 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 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 = 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 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”表示访问令牌将在响应生成后一小时内过期
然后在访问:访问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 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
源码地址:https://gitee.com/niugangxy/springboot
微信公众号:
JAVA程序猿成长之路
分享资源,记录程序猿成长点滴。专注于Java,Spring,SpringBoot,SpringCloud,分布式,微服务。