oauth2 sso 大致流程
在一个公司中,肯定会存在多个不同的应用,比如公司的OA系统,HR系统等等,如果每个系统都用独立的账号认证体系,会给用户带来很大困扰,也给管理带来很大不便。所以通常需要设计一种统一登录的解决方案。比如我登陆了OA系统账号,进入HR系统时发现已经登录了,进入公司其他系统发现也自动登录了。使用SSO解决效果是一次输入密码多个应用都可以识别在线状态。
- 浏览器向客户端服务器请求接口触发要求安全认证
- 跳转到授权服务器获取授权许可码
- 从授权服务器带授权许可码跳回来
- 客户端服务器向授权服务器获取AccessToken
- 返回AccessToken到客户端服务器
- 发出/resource请求到客户端服务器
- 客户端服务器将/resource请求转发到Resource服务器
- Resource服务器要求安全验证,于是直接从授权服务器获取认证授权信息进行判断后(最后会响应给客户端服务器,客户端服务器再响应给浏览中器)
SSO 角色
- 统一认证服务 AuthorizationServer
- SSO 客户端 OAuth2Sso
工程结构
认证服务实现
工程结构
pom.xml
UTF-8
1.0.9.RELEASE
0.9.0
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.cloud
spring-cloud-starter-security
org.springframework.security
spring-security-jwt
${security-jwt.version}
io.jsonwebtoken
jjwt
${jjwt.version}
com.alibaba
fastjson
1.2.47
org.postgresql
postgresql
runtime
org.springframework.boot
spring-boot-devtools
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-freemarker
org.apache.commons
commons-lang3
3.7
org.springframework.boot
spring-boot-starter-test
test
application.yml
server:
port: 18082
spring:
application:
name: oauth2-server # 应用名称
jpa:
open-in-view: true
database: POSTGRESQL
show-sql: true
hibernate:
ddl-auto: update
dialect: org.hibernate.dialect.PostgreSQLDialect
properties:
hibernate:
temp:
use_jdbc_metadata_defaults: false
# 数据源 配置
datasource:
platform: postgres
url: jdbc:postgresql://127.0.0.1:5432/cloud_oauth2?useUnicode=true&characterEncoding=utf-8
username: postgres
password: postgres123
driver-class-name: org.postgresql.Driver
redis:
host: 127.0.0.1
database: 0
thymeleaf:
prefix: classpath:/static/pages/
# 不需要拦截的url地址
mySecurity:
exclude:
antMatchers: /oauth/**,/login,/home
logging:
level:
org.springframework.security: DEBUG
Security 登录身份证认证
@Slf4j
@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService {
@Autowired
private SysAccountRepository repository;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysAccount user = repository.findByUserAccount(username);
if(user == null){
log.info("登录用户【"+username + "】不存在.");
throw new UsernameNotFoundException("登录用户【"+username + "】不存在.");
}
return new org.springframework.security.core.userdetails.User(user.getUserAccount(), user.getUserPwd(), getAuthority());
}
private List getAuthority() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
}
权限认证服务配置 AuthorizationServerConfiguration
/***
* 身份授权认证服务配置
* 配置客户端、token存储方式等
*/
@Configuration
@EnableAuthorizationServer // 注解开启验证服务器 提供/oauth/authorize,/oauth/token,/oauth/check_token,/oauth/confirm_access,/oauth/error
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private static final String REDIRECT_URL = "https://www.baidu.com/";
private static final String CLIEN_ID_THREE = "client_3"; //客户端3
private static final String CLIENT_SECRET = "secret"; //secret客户端安全码
private static final String GRANT_TYPE_PASSWORD = "password"; // 密码模式授权模式
private static final String AUTHORIZATION_CODE = "authorization_code"; //授权码模式 授权码模式使用到了回调地址,是最为复杂的方式,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。
private static final String REFRESH_TOKEN = "refresh_token"; //
private static final String IMPLICIT = "implicit"; //简化授权模式
private static final String GRANT_TYPE = "client_credentials"; //客户端模式
private static final String SCOPE_READ = "read";
private static final String SCOPE_WRITE = "write";
private static final String TRUST = "trust";
private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1*60*60; //
private static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60; //
private static final String RESOURCE_ID = "resource_id"; //指定哪些资源是需要授权验证的
@Autowired
private AuthenticationManager authenticationManager; //认证方式
@Resource(name = "userService")
private UserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
String secret = new BCryptPasswordEncoder().encode(CLIENT_SECRET); // 用 BCrypt 对密码编码
//配置3个个客户端,一个用于password认证、一个用于client认证、一个用于authorization_code认证
configurer.inMemory() // 使用in-memory存储
.withClient(CLIEN_ID_THREE) //client_id用来标识客户的Id 客户端3
.resourceIds(RESOURCE_ID)
.authorizedGrantTypes(AUTHORIZATION_CODE,GRANT_TYPE, REFRESH_TOKEN,GRANT_TYPE_PASSWORD,IMPLICIT) //允许授权类型
.scopes(SCOPE_READ,SCOPE_WRITE,TRUST) //允许授权范围
.authorities("ROLE_CLIENT") //客户端可以使用的权限
.secret(secret) //secret客户端安全码
//.redirectUris(REDIRECT_URL) //指定可以接受令牌和授权码的重定向URIs
.autoApprove(true) // 为true 则不会被重定向到授权的页面,也不需要手动给请求授权,直接自动授权成功返回code
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS) //token 时间秒
.refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS);//刷新token 时间 秒
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter())
.userDetailsService(userDetailsService) //必须注入userDetailsService否则根据refresh_token无法加载用户信息
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST,HttpMethod.OPTIONS) //支持GET POST 请求获取token
.reuseRefreshTokens(true) //开启刷新token
.tokenServices(tokenServices());
}
/**
* 认证服务器的安全配置
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.realm(RESOURCE_ID)
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()") //isAuthenticated():排除anonymous isFullyAuthenticated():排除anonymous以及remember-me
.allowFormAuthenticationForClients(); //允许表单认证 这段代码在授权码模式下会导致无法根据code 获取token
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
/**
* 自定义一些token返回的信息
* @param accessToken
* @param authentication
* @return
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String grantType = authentication.getOAuth2Request().getGrantType();
//只有如下两种模式才能获取到当前用户信息
if("authorization_code".equals(grantType) || "password".equals(grantType)) {
String userName = authentication.getUserAuthentication().getName();
// 自定义一些token 信息 会在获取token返回结果中展示出来
final Map additionalInformation = new HashMap<>();
additionalInformation.put("user_name", userName);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
}
OAuth2AccessToken token = super.enhance(accessToken, authentication);
return token;
}
};
converter.setSigningKey("bcrypt");
return converter;
}
@Bean
public TokenStore tokenStore() {
//基于jwt实现令牌(Access Token)
return new JwtTokenStore(accessTokenConverter());
}
/**
* 重写默认的资源服务token
* @return
*/
@Bean
public DefaultTokenServices tokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenEnhancer(accessTokenConverter());
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)); // 30天
return defaultTokenServices;
}
}
资源服务认证配置 ResourceServerConfiguration
@Configuration
@EnableResourceServer //注解来开启资源服务器
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "resource_id";
@Autowired
private DefaultTokenServices tokenServices;
@Autowired
private TokenStore tokenStore;
@Autowired
private PermitAuthenticationFilter permitAuthenticationFilter;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID).stateless(true).tokenServices(tokenServices);
}
@Override
public void configure(HttpSecurity http) throws Exception {
// 配置那些资源需要保护的
http.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler()) //权限认证失败业务处理
.authenticationEntryPoint(customAuthenticationEntryPoint()); //认证失败的业务处理
http.addFilterBefore(permitAuthenticationFilter,AbstractPreAuthenticatedProcessingFilter.class); //自定义token过滤 token校验失败后自定义返回数据格式
}
@Bean
public LogoutSuccessHandler customLogoutSuccessHandler(){
return new CustomLogoutSuccessHandler();
}
@Bean
public AuthenticationFailureHandler customLoginFailHandler(){
return new CustomLoginFailHandler();
}
@Bean
public OAuth2AuthenticationEntryPoint customAuthenticationEntryPoint(){
return new CustomAuthenticationEntryPoint();
}
@Bean
public OAuth2AccessDeniedHandler customAccessDeniedHandler(){
return new CustomAccessDenieHandler();
}
/**
* 重写 token 验证失败后自定义返回数据格式
* @return
*/
@Bean
public WebResponseExceptionTranslator webResponseExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
@Override
public ResponseEntity translate(Exception e) throws Exception {
ResponseEntity responseEntity = super.translate(e);
OAuth2Exception body = (OAuth2Exception) responseEntity.getBody();
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
// do something with header or response
if(401==responseEntity.getStatusCode().value()){
//自定义返回数据格式
Map map = new HashMap<>();
map.put("status","401");
map.put("message",e.getMessage());
map.put("timestamp", String.valueOf(LocalDateTime.now()));
return new ResponseEntity(JSON.toJSONString(map), headers, responseEntity.getStatusCode());
}else{
return new ResponseEntity(body, headers, responseEntity.getStatusCode());
}
}
};
}
}
web安全配置 SecurityConfiguration
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableAutoConfiguration(exclude = {
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class })
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private SimpleCORSFilter simpleCORSFilter;
@Resource(name = "userService")
private UserDetailsService userDetailsService;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder());
auth.parentAuthenticationManager(authenticationManagerBean());
}
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/assets/**");
web.ignoring().antMatchers("/favicon.ico");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.requestMatchers() // requestMatchers 配置 数组
.antMatchers("/oauth/**","/login","/home")
.and()
.authorizeRequests() //authorizeRequests 配置权限 顺序为先配置需要放行的url 在配置需要权限的url,最后再配置.anyRequest().authenticated()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
http.addFilterBefore(simpleCORSFilter, SecurityContextPersistenceFilter.class);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
跨域设置 SimpleCORSFilter
@Slf4j
@Component
public class SimpleCORSFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
httpServletRequest.setCharacterEncoding("utf-8");
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setHeader("Content-Type", "application/json");
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");//允许所以域名访问,
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");//允许的访问方式
httpServletResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with,content-type,Authorization");
httpServletResponse.setHeader("Access-Control-Request-Headers", "x-requested-with,content-type,Accept,Authorization");
httpServletResponse.setHeader("Access-Control-Request-Method", "GET,POST,PUT,DELETE,OPTIONS");
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
自定义过滤器验证token 返回自定义数据格式 PermitAuthenticationFilter
@Slf4j
@Component
public class PermitAuthenticationFilter extends OAuth2AuthenticationProcessingFilter {
private static final String BEARER_AUTHENTICATION = "Bearer ";
private static final String HEADER_AUTHORIZATION = "authorization";
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
private boolean stateless = true;
OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
@Autowired
private TokenStore tokenStore;
public PermitAuthenticationFilter() {
DefaultTokenServices dt = new DefaultTokenServices();
dt.setTokenStore(tokenStore);
oAuth2AuthenticationManager.setTokenServices(dt);
this.setAuthenticationManager(oAuth2AuthenticationManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest)servletRequest;
log.info(" ================== =========================== ===================");
log.info("当前访问的URL地址:" +request.getRequestURI());
Authentication authentication = this.tokenExtractor.extract(request);
if (authentication == null) {
if (this.stateless && this.isAuthenticated()) {
// SecurityContextHolder.clearContext();
}
log.info("当前访问的URL地址:" +request.getRequestURI() +"不进行拦截...");
filterChain.doFilter(request, response);
} else {
log.info("************ 开始验证token .......................... ");
String accessToken = request.getParameter("access_token");
String headerToken = request.getHeader(HEADER_AUTHORIZATION);
Map map = new HashMap<>();
map.put("status","403");
AtomicBoolean error = new AtomicBoolean(false);
if(StringUtils.isNotBlank(accessToken)){
try {
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
log.info("token =" +oAuth2AccessToken.getValue());
}catch (InvalidTokenException e){
error.set(true);
map.put("message",e.getMessage());
log.info("** 无校的token信息. ** ");
// throw new AccessDeniedException("无校的token信息.");
}
}else if (StringUtils.isNotBlank(headerToken) && headerToken.startsWith(BEARER_AUTHENTICATION)){
try {
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(headerToken.split(" ")[0]);
log.info("token =" +oAuth2AccessToken.getValue());
}catch (InvalidTokenException e){
error.set(true);
map.put("message",e.getMessage());
log.info("** 无校的token信息. ** ");
// throw new AccessDeniedException("无校的token信息.");
}
}else {
error.set(true);
map.put("message","参数无token.");
log.info("** 参数无token. ** ");
//throw new AccessDeniedException("参数无token.");
}
if (!error.get()){
filterChain.doFilter(request, response);
}else {
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(LocalDateTime.now()));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ResultUtil.writeJavaScript(response,map);
}
}
}
@Override
public void destroy() {
}
private boolean isAuthenticated() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null && !(authentication instanceof AnonymousAuthenticationToken);
}
}
页面跳转url注册 MvcConfig
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/login").setViewName("login"); //自定义的登陆页面
registry.addViewController("/oauth/confirm_access").setViewName("oauth_approval"); //自定义的授权页面
registry.addViewController("/oauth_error").setViewName("oauth_error");
}
}
自定义身份证认证失败返回数据格式 CustomAuthenticationEntryPoint
@Slf4j
@Component
public class CustomAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.info(" ====================================================== ");
log.info("请求url:" +httpServletRequest.getRequestURI());
log.info(" ============ 身份认证失败..................... ");
log.info(e.getMessage());
log.info(e.getLocalizedMessage());
e.printStackTrace();
Map map = new HashMap<>();
map.put("status","401");
map.put("message",e.getMessage());
map.put("path", httpServletRequest.getServletPath());
map.put("timestamp", String.valueOf(LocalDateTime.now()));
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ResultUtil.writeJavaScript(httpServletResponse,map);
}
}
测试 Controller UserController
@CrossOrigin
@RestController
public class UserController {
@GetMapping("oauth/me")
public Principal getUser(Principal user){
System.out.println(".. 进入 获取用户信息 方法 .......... ");
System.out.println(JSON.toJSONString(user));
return user;
}
@GetMapping("api/user")
public Principal user(Principal user){
System.out.println(".. 进入 获取用户信息 方法 .......... ");
System.out.println(JSON.toJSONString(user));
return user;
}
@RequestMapping(path = "api/messages", method = RequestMethod.GET)
public List getMessages(Principal principal) {
List list = new LinkedList<>();
list.add("俏如来");
list.add("帝如来");
list.add("鬼如来");
return list;
}
@RequestMapping(path = "api/messages", method = RequestMethod.POST)
public String postMessage(Principal principal) {
return "POST -> 默苍离 ";
}
/**
* 当前登录人信息
* @return
*/
@RequestMapping(path = "api/loginUser", method = RequestMethod.GET)
public UserDetails currentlyLoginUser(){
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDetails;
}
}
把字符串数据输出到页面 ResultUtil
public class ResultUtil {
/**
* 将json输出到前端(参数非json格式)
* @param response
* @param obj 任意类型
*/
public static void writeJavaScript(HttpServletResponse response, Object obj){
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Cache-Control","no-store, max-age=0, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
/* 设置浏览器跨域访问 */
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,Authorization");
response.setHeader("Access-Control-Allow-Credentials","true");
try {
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(obj));
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
启动类 SecurityOauth2AuthorizationServerApplication
@SpringBootApplication
public class SecurityOauth2AuthorizationServerApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityOauth2AuthorizationServerApplication.class, args);
}
}
登录html页面 login.html
OAuth2 SSO login
login.html 效果图:
首页 html 页面 home.html
OAuth2 SSO login
home.html 效果图
SSO client 客户端
项目结构
pom.xml
UTF-8
1.0.9.RELEASE
0.9.0
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.cloud
spring-cloud-starter-security
org.springframework.security
spring-security-jwt
${security-jwt.version}
com.alibaba
fastjson
1.2.47
io.jsonwebtoken
jjwt
${jjwt.version}
org.postgresql
postgresql
runtime
org.springframework.boot
spring-boot-devtools
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
application.yml
server:
port: 18083
spring:
application:
name: oauth2-sso-client1 # 应用名称
jpa:
open-in-view: true
database: POSTGRESQL
show-sql: true
hibernate:
ddl-auto: update
dialect: org.hibernate.dialect.PostgreSQLDialect
properties:
hibernate:
temp:
use_jdbc_metadata_defaults: false
# 数据源 配置
datasource:
platform: postgres
url: jdbc:postgresql://127.0.0.1:5432/cloud_oauth2?useUnicode=true&characterEncoding=utf-8
username: postgres
password: postgres123
driver-class-name: org.postgresql.Driver
redis:
host: 127.0.0.1
database: 0
thymeleaf:
prefix: classpath:/static/pages/
#cache: false
security:
user:
name: user
password: e94a652b-adfb-4af7-ba00-d88419289172
# sso 认证配置
oauth2-server: http://localhost:18082
security:
oauth2:
client:
# grant-type: client_credentials # 授权模式
client-id: client_3 # 在oauth 服务端注册的client-id
client-secret: secret # 在oauth 服务端注册的secret
access-token-uri: ${oauth2-server}/oauth/token #获取token 地址
user-authorization-uri: ${oauth2-server}/oauth/authorize # 认证地址
scope: read,write
resource:
token-info-uri: ${oauth2-server}/oauth/check_token # 检查token
user-info-uri: ${oauth2-server}/oauth/me # 用户信息
jwt:
key-uri: ${oauth2-server}/oauth/token_key
sso:
login-path: /login
logging:
level:
org.springframework.security: DEBUG
web 安全配置 SecurityConfiguration
@Configuration
@EnableWebSecurity
@EnableOAuth2Sso //@EnableOAuth2Sso注解来开启SSO
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private SimpleCORSFilter simpleCORSFilter;
@Value("${oauth2-server}")
private String oauthServerUrl;
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/assets/**");
web.ignoring().antMatchers("/favicon.ico");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.requestMatchers()
.antMatchers("/oauth/**","/login","/index")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler())
.and()
.formLogin()
.permitAll()
.loginProcessingUrl("/index");
http.addFilterBefore(simpleCORSFilter,SecurityContextPersistenceFilter.class);
}
}
资源配置 ResourceConfiguration
@Configuration
@EnableResourceServer //注解来开启资源服务器
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "resource_id";
@Autowired
private DefaultTokenServices tokenServices;
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID).stateless(true).tokenServices(tokenServices);
}
@Override
public void configure(HttpSecurity http) throws Exception {
//如果 启用 http.addFilterBefore(oAuth2AuthenticationFilter,AbstractPreAuthenticatedProcessingFilter.class) 代码 则需要启用下面被注释的代码
OAuth2AuthenticationProcessingFilter oAuth2AuthenticationFilter = new OAuth2AuthenticationProcessingFilter();
OAuth2AuthenticationEntryPoint oAuth2AuthenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
oAuth2AuthenticationEntryPoint.setExceptionTranslator(webResponseExceptionTranslator());
oAuth2AuthenticationFilter.setAuthenticationEntryPoint(oAuth2AuthenticationEntryPoint);
OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore);
oAuth2AuthenticationManager.setTokenServices(defaultTokenServices);
oAuth2AuthenticationFilter.setAuthenticationManager(oAuth2AuthenticationManager);
// 配置那些资源需要保护的
http.csrf().disable()
.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
http.addFilterBefore(oAuth2AuthenticationFilter,AbstractPreAuthenticatedProcessingFilter.class); // 这种方式也可以达到token校验失败后自定义返回数据格式 使用此方式需要将上面的代码启用
}
/**
* 重写 token 验证失败后自定义返回数据格式
* @return
*/
@Bean
public WebResponseExceptionTranslator webResponseExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
@Override
public ResponseEntity translate(Exception e) throws Exception {
ResponseEntity responseEntity = super.translate(e);
OAuth2Exception body = (OAuth2Exception) responseEntity.getBody();
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
// do something with header or response
if(401==responseEntity.getStatusCode().value()){
//自定义返回数据格式
Map map = new HashMap<>();
map.put("status","401");
map.put("message",e.getMessage());
map.put("timestamp", String.valueOf(LocalDateTime.now()));
return new ResponseEntity(JSON.toJSONString(map), headers, responseEntity.getStatusCode());
}else{
return new ResponseEntity(body, headers, responseEntity.getStatusCode());
}
}
};
}
}
跨域 SimpleCORSFilter
@Component
public class SimpleCORSFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
String token = req.getParameter("access_token");
System.out.println(" token -- "+ token);
if(!StringUtils.isEmpty(token)){
TokenContextHolder.setToken(token);
}
HttpServletResponse response = (HttpServletResponse) res;
response.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Type", "application/json");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,content-type,Accept,Authorization");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
}
页面跳转url 注册 MvcConfiguration
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index").setViewName("index");
registry.addViewController("/securedPage");
}
}
token 存放 TokenContextHolder
public class TokenContextHolder {
private static final ThreadLocal LOCAL_TOKEN = new ThreadLocal<>();
public static void setToken(String value){
LOCAL_TOKEN.set(value);
}
public static String getToken(){
String token = LOCAL_TOKEN.get();
clearToken();
return token;
}
public static void clearToken(){
LOCAL_TOKEN.remove();
}
}
测试Controller HomeController
@CrossOrigin
@RestController
public class HomeController {
@Value("${oauth2-server}")
private String serverUrl;
@Autowired
IMessageService messageService;
@Autowired
OAuth2RestTemplate oAuth2RestTemplate;
@RequestMapping("/getMessages")
public List getMessages(){
List list = oAuth2RestTemplate.getForObject(serverUrl+"/api/messages",List.class);
list.stream().forEach(item ->{
System.out.println(item);
});
return list;
}
@RequestMapping("api/test")
public String test(){
Map map = new HashMap<>();
map.put("code","0");
map.put("msg","测试权限信息成功");
System.out.println(JSON.toJSONString(map));
return JSON.toJSONString(map);
}
@RequestMapping("/postMessages")
public String postMessage(){
String token = TokenContextHolder.getToken();
String str = oAuth2RestTemplate.postForObject(serverUrl+"api/messages?access_token="+token,null,String.class);
Map map = new HashMap<>();
map.put("msg",str);
System.out.println(JSON.toJSONString(map));
return JSON.toJSONString(map);
}
@GetMapping("api/user")
public String user(){
System.out.println(".. 进入 获取用户信息 方法 .......... ");
String token = TokenContextHolder.getToken();
String str = oAuth2RestTemplate.getForObject(serverUrl+"api/user?access_token="+token,String.class);
System.out.println(JSON.toJSONString(str));
return JSON.toJSONString(str);
}
}
启动类 Oauth2SsoClient1Application
@SpringBootApplication
public class Oauth2SsoClient1Application {
@Bean
OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext, OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
}
public static void main(String[] args) {
SpringApplication.run(Oauth2SsoClient1Application.class, args);
}
}
html 页面 index.html
OAuth2 SSO Demo
Spring Security SSO
Login
authorization_code
获取其他服务登录人接口信息
localhost:18082服务的登录人信息
获取自身服务登录人接口信息
localhost:18083服务的登录人信息
获取自身postMessages接口信息
index.html 效果图
1. 请求授权访问18083端口应用服务
http://localhost:18082/oauth/authorize?response_type=code&client_id=client_3&redirect_uri=http://localhost:18083/index
如果处于未登录状态则会跳转到认证服务器的登录页面
2. 登录成功后回跳到http://localhost:18083/index 页面 并且携带code值
3. 根据code 值 获取token
$.ajax({
url:"http://localhost:18082/oauth/token?grant_type=authorization_code&client_id=client_3&client_secret=secret&redirect_uri=http://localhost:18083/index&code="+code,
type:'get',
dataType:'json',
withCredentials: true,
success:function(data,textStatus,XMLHttpRequest){
console.log(data);
access_token = data.access_token;
},
error:function(xhr,status,error){
toastr.error("请求获取token出现错误.");
}
});
4. 携带token 访问认证服务端的资源接口
$.ajax({
url:"http://localhost:18082/api/user",
data:{
"access_token":access_token
},
type:'get',
dataType:'json',
withCredentials: true,
success:function(data,textStatus,XMLHttpRequest){
console.log(data);
App.alert({
container: "#user_info",
message:JSON.stringify(data),
close: true,
icon: 'fa fa-user',
closeInSeconds: 1000
});
},
error:function(xhr,status,error){
var obj = JSON.parse(xhr.responseText);
toastr.error(obj.message);
}
});
返回数据:
5. 访问授权18082端口应用
window.open("http://localhost:18082/oauth/authorize?response_type=code&client_id=client_3&redirect_uri=http://localhost:18082/home");
如果已经处于登录状态 则直接进入http://localhost:18082/home页面
6. 携带token 访问18083端口服务接口资源
$.ajax({
url:"http://localhost:18083/api/user",
data:{
"access_token":access_token
},
type:'get',
dataType:'json',
withCredentials: true,
success:function(data,textStatus,XMLHttpRequest){
console.log(data);
App.alert({
container: "#user_info",
message:JSON.stringify(data),
close: true,
icon: 'fa fa-user',
closeInSeconds: 1000
});
toastr.success("登录人信息",JSON.stringify(data));
},
error:function(xhr,status,error){
console.log(xhr);
toastr.error("请求获取localhost:18083/api/user服务登录人信息接口出错.");
}
});
})
返回数据
7. 当请求访问资源未携带token 认证服务会进入 CustomAuthenticationEntryPoint 类中
8. 当前请求携带错误的token 会在 PermitAuthenticationFilter 验证处理
8. 未登录状态下访问sso client 客户端资源接口或者认证服务端的资源接口时 都会跳转到登录页面去
demo地址: