Spring boot 2.0 整合 oauth2 SSO

oauth2 sso 大致流程

在一个公司中,肯定会存在多个不同的应用,比如公司的OA系统,HR系统等等,如果每个系统都用独立的账号认证体系,会给用户带来很大困扰,也给管理带来很大不便。所以通常需要设计一种统一登录的解决方案。比如我登陆了OA系统账号,进入HR系统时发现已经登录了,进入公司其他系统发现也自动登录了。使用SSO解决效果是一次输入密码多个应用都可以识别在线状态。

  1. 浏览器向客户端服务器请求接口触发要求安全认证
  2. 跳转到授权服务器获取授权许可码
  3. 从授权服务器带授权许可码跳回来
  4. 客户端服务器向授权服务器获取AccessToken
  5. 返回AccessToken到客户端服务器
  6. 发出/resource请求到客户端服务器
  7. 客户端服务器将/resource请求转发到Resource服务器
  8. Resource服务器要求安全验证,于是直接从授权服务器获取认证授权信息进行判断后(最后会响应给客户端服务器,客户端服务器再响应给浏览中器)

SSO 角色

  1. 统一认证服务 AuthorizationServer
  2. SSO 客户端 OAuth2Sso

工程结构

image.png

认证服务实现

工程结构

image.png

pom.xml

  <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <security-jwt.version>1.0.9.RELEASE</security-jwt.version>
        <jjwt.version>0.9.0</jjwt.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>${security-jwt.version}</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

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<String, Object> 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<String,String> 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<String,String> 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<String,String> 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<String> getMessages(Principal principal) {
        List<String> 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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>OAuth2 SSO login</title>
    <link href="../assets/global/plugins/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/simple-line-icons/simple-line-icons.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/bootstrap-switch/css/bootstrap-switch.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/css/components.min.css" rel="stylesheet" id="style_components" type="text/css" />
    <link href="../assets/global/css/plugins.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/pages/css/login.min.css" rel="stylesheet" type="text/css" />
</head>

<body class=" login">

<div class="content" style="margin-top: 10%">
    <!-- BEGIN LOGIN FORM -->
    <form class="login-form" action="login" role="form" method="post">
        <h3 class="form-title">Login to your account</h3>
        <div class="alert alert-danger display-hide">
            <button class="close" data-close="alert"></button>
            <span> Enter any username and password. </span>
        </div>
        <div class="form-group">
            <!--ie8, ie9 does not support html5 placeholder, so we just show field title for that-->
            <label class="control-label visible-ie8 visible-ie9">Username</label>
            <div class="input-icon">
                <i class="fa fa-user"></i>
                <input class="form-control placeholder-no-fix" type="text" autocomplete="off" placeholder="Username" name="username" value="mocangli"/> </div>
        </div>
        <div class="form-group">
            <label class="control-label visible-ie8 visible-ie9">Password</label>
            <div class="input-icon">
                <i class="fa fa-lock"></i>
                <input class="form-control placeholder-no-fix" type="password" autocomplete="off" placeholder="Password" name="password" value="123456"/> </div>
        </div>
        <div class="form-actions">
            <label class="rememberme mt-checkbox mt-checkbox-outline">
                <input type="checkbox" name="remember" value="1" /> Remember me
                <span></span>
            </label>
            <button type="submit" id="login-button" class="btn green pull-right"> Login </button>
           <!-- <button type="button" id="login-button" class="btn green pull-right"> Login </button>-->
        </div>
    </form>
    <!-- END LOGIN FORM -->
</div>
<script src="../assets/global/plugins/jquery.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/js.cookie.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/jquery-slimscroll/jquery.slimscroll.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/jquery.blockui.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/bootstrap-switch/js/bootstrap-switch.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/backstretch/jquery.backstretch.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/jquery-validation/js/jquery.validate.min.js" type="text/javascript"></script>
<script src="../assets/global/scripts/app.min.js" type="text/javascript"></script>
</body>
</html>

login.html 效果图:


image.png

首页 html 页面 home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>OAuth2 SSO login</title>
    <link href="../assets/global/plugins/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/simple-line-icons/simple-line-icons.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/bootstrap-switch/css/bootstrap-switch.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/css/components.min.css" rel="stylesheet" id="style_components" type="text/css" />
    <link href="../assets/global/css/plugins.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/bootstrap-toastr/toastr.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/layouts/layout/css/layout.min.css" rel="stylesheet" type="text/css">
    <link href="../assets/layouts/layout/css/themes/darkblue.min.css" rel="stylesheet" type="text/css" id="style_color">
    <link href="../assets/layouts/layout/css/custom.css" rel="stylesheet" type="text/css" />
</head>

<body class=" page-header-fixed page-sidebar-closed-hide-logo page-content-white">

    <div class="page-content-wrapper">
        <div class="page-content">
            <div class="row">
                <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
                    <a class="dashboard-stat dashboard-stat-v2 blue" href="#">
                        <div class="visual">
                            <i class="fa fa-comments"></i>
                        </div>
                        <div class="details">
                            <div class="number">
                                <span data-counter="counterup" data-value="client_1">client_1</span>
                            </div>
                            <div class="desc" style="padding-top: 10px">
                                <button type="button" id="client_1_btn" class="btn green mt-ladda-btn ladda-button btn-circle btn-outline" data-style="slide-down" data-spinner-color="#333">
                                                        <span class="ladda-label">
                                                           点击进入<i class="fa fa-arrow-circle-right"></i> </span>
                                    <span class="ladda-spinner"></span></button>
                            </div>
                        </div>
                    </a>
                </div>
                <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
                    <a class="dashboard-stat dashboard-stat-v2 green" href="#">
                        <div class="visual">
                            <i class="fa fa-shopping-cart"></i>
                        </div>
                        <div class="details">
                            <div class="number">
                                <span data-counter="counterup" data-value="client_2">client_2</span>
                            </div>
                            <div class="desc" style="padding-top: 10px">
                                <button type="button" id="client_2_btn" class="btn blue mt-ladda-btn ladda-button btn-circle btn-outline" data-style="slide-down" data-spinner-color="#333">
                                                        <span class="ladda-label">
                                                            点击进入<i class="fa fa-arrow-circle-right"></i> </span>
                                    <span class="ladda-spinner"></span></button>
                            </div>
                        </div>
                    </a>
                </div>
                <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
                    <a class="dashboard-stat dashboard-stat-v2 purple" href="#">
                        <div class="visual">
                            <i class="fa fa-globe"></i>
                        </div>
                        <div class="details">
                            <div class="number">
                                <span data-counter="counterup" data-value="client_3">client_3</span>
                            </div>
                            <div class="desc" style="padding-top: 10px">
                                <button type="button" id="client_3_btn" class="btn green mt-ladda-btn ladda-button btn-circle btn-outline" data-style="slide-down" data-spinner-color="#333">
                                                        <span class="ladda-label">
                                                            点击进入<i class="fa fa-arrow-circle-right"></i> </span>
                                    <span class="ladda-spinner"></span></button>
                            </div>
                        </div>
                    </a>
                </div>
            </div>
        </div>
    </div>
<script src="../assets/global/plugins/jquery.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/js.cookie.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/jquery-slimscroll/jquery.slimscroll.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/jquery.blockui.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/bootstrap-switch/js/bootstrap-switch.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/backstretch/jquery.backstretch.min.js" type="text/javascript"></script>
    <script src="../assets/global/plugins/bootstrap-toastr/toastr.min.js" type="text/javascript"></script>
<script src="../assets/global/scripts/app.min.js" type="text/javascript"></script>
<script src="../assets/layouts/layout/scripts/layout.min.js" type="text/javascript"></script>
    <script src="../assets/pages/scripts/home.js" type="text/javascript"></script>

</body>
</html>

home.html 效果图


image.png

SSO client 客户端

项目结构

image.png

pom.xml

  <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <security-jwt.version>1.0.9.RELEASE</security-jwt.version>
        <jjwt.version>0.9.0</jjwt.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>${security-jwt.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

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<String,String> 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<String> 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<String> getMessages(){
        List<String> 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<String,String> 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<String,String> 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

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>OAuth2 SSO Demo</title>
    <link href="../assets/global/plugins/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/simple-line-icons/simple-line-icons.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/bootstrap-switch/css/bootstrap-switch.min.css" rel="stylesheet" type="text/css" />
    <link href="../assets/global/plugins/bootstrap-toastr/toastr.min.css" rel="stylesheet" type="text/css" />
    <link href="https://cdn.bootcss.com/layer/3.1.0/theme/default/layer.css" rel="stylesheet">
</head>

<body>
<div class="container">
    <div class="row">
        <div class="col-sm-12">
            <h1>Spring Security SSO</h1>
            <a class="btn btn-primary" href="securedPage" id="sso_btn">Login</a>
        </div>
    </div>

    <div class="row">
        <div class="col-sm-12">
            <h1>authorization_code</h1>
            <button type="button" id="request_auth_code_btn" class="btn btn-primary mt-ladda-btn ladda-button btn-circle" data-style="expand-right" onclick="Index.authorization()">
                <span class="ladda-label">
                    请求访问其他客户端资源 
                    <i class="icon-arrow-right"></i>
                </span>
                <span class="ladda-spinner"></span>
            </button>
        </div>
    </div>

    <div class="row">
        <div class="col-sm-12">
            <h1>获取其他服务登录人接口信息</h1>
            <button type="button" id="user_btn" class="btn btn-primary mt-ladda-btn ladda-button btn-circle" data-style="expand-right" onclick="Index.userInfo()">
                <span class="ladda-label">
                    获取localhost:18082服务的当前登录人信息 
                    <i class="icon-arrow-right"></i>
                </span>
                <span class="ladda-spinner"></span>
            </button>
        </div>
    </div>

    <div class="row">
        <div class="col-sm-12">
            <h1>localhost:18082服务的登录人信息</h1>
            <div id="user_info"></div>
        </div>
    </div>


    <div class="row">
        <div class="col-sm-12">
            <h1>获取自身服务登录人接口信息</h1>
            <button type="button" id="localhost_user_btn" class="btn btn-primary mt-ladda-btn ladda-button btn-circle" data-style="expand-right" onclick="Index.localUserInfo()">
                <span class="ladda-label">
                    获取localhost:18083服务的当前登录人信息 
                    <i class="icon-arrow-right"></i>
                </span>
                <span class="ladda-spinner"></span>
            </button>
        </div>
    </div>

    <div class="row">
        <div class="col-sm-12">
            <h1>localhost:18083服务的登录人信息</h1>
            <div id="localhost_user_info"></div>
        </div>
    </div>

    <div class="row">
        <div class="col-sm-12">
            <h1>获取自身postMessages接口信息</h1>
            <button type="button" id="localhost_msg_user_btn" class="btn btn-primary mt-ladda-btn ladda-button btn-circle" data-style="expand-right" onclick="Index.localMsgInfo()">
                <span class="ladda-label">
                    获取localhost:18083服务的postMessages接口信息 
                    <i class="icon-arrow-right"></i>
                </span>
                <span class="ladda-spinner"></span>
            </button>
        </div>
    </div>

    <div id="loginModal"  class="modal fade" role="dialog" tabindex="-1">

    </div>

</div>

<script src="../assets/global/plugins/jquery.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/jquery.blockui.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/bootstrap-switch/js/bootstrap-switch.min.js" type="text/javascript"></script>
<script src="../assets/global/plugins/bootstrap-toastr/toastr.min.js" type="text/javascript"></script>
<script src="../assets/global/scripts/app.min.js" type="text/javascript"></script>
<script src="https://cdn.bootcss.com/layer/3.1.0/layer.js"></script>

<script src="../assets/pages/scripts/index.js" type="text/javascript"></script>
</body>
</html>

index.html 效果图


image.png

1. 请求授权访问18083端口应用服务

http://localhost:18082/oauth/authorize?response_type=code&client_id=client_3&redirect_uri=http://localhost:18083/index
如果处于未登录状态则会跳转到认证服务器的登录页面

image.png

2. 登录成功后回跳到http://localhost:18083/index 页面 并且携带code值

image.png

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);
            }
        });

返回数据:


image.png

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页面

image.png

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服务登录人信息接口出错.");
                }
            });
        })

返回数据


image.png

7. 当请求访问资源未携带token 认证服务会进入 CustomAuthenticationEntryPoint 类中

image.png
image.png

8. 当前请求携带错误的token 会在 PermitAuthenticationFilter 验证处理

image.png

image.png

8. 未登录状态下访问sso client 客户端资源接口或者认证服务端的资源接口时 都会跳转到登录页面去

image.png

image.png

image.png

demo地址:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,814评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,124评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,814评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,924评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,815评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,562评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,944评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,582评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,859评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,881评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,700评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,493评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,943评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,115评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,413评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,978评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,182评论 2 339

推荐阅读更多精彩内容