Spring boot 2.0 整合 oauth2 SSO

oauth2 sso 大致流程

在一個(gè)公司中失暂,肯定會(huì)存在多個(gè)不同的應(yīng)用遥金,比如公司的OA系統(tǒng)临燃,HR系統(tǒng)等等睛驳,如果每個(gè)系統(tǒng)都用獨(dú)立的賬號(hào)認(rèn)證體系,會(huì)給用戶帶來(lái)很大困擾膜廊,也給管理帶來(lái)很大不便乏沸。所以通常需要設(shè)計(jì)一種統(tǒng)一登錄的解決方案。比如我登陸了OA系統(tǒng)賬號(hào)爪瓜,進(jìn)入HR系統(tǒng)時(shí)發(fā)現(xiàn)已經(jīng)登錄了蹬跃,進(jìn)入公司其他系統(tǒng)發(fā)現(xiàn)也自動(dòng)登錄了。使用SSO解決效果是一次輸入密碼多個(gè)應(yīng)用都可以識(shí)別在線狀態(tài)铆铆。

  1. 瀏覽器向客戶端服務(wù)器請(qǐng)求接口觸發(fā)要求安全認(rèn)證
  2. 跳轉(zhuǎn)到授權(quán)服務(wù)器獲取授權(quán)許可碼
  3. 從授權(quán)服務(wù)器帶授權(quán)許可碼跳回來(lái)
  4. 客戶端服務(wù)器向授權(quán)服務(wù)器獲取AccessToken
  5. 返回AccessToken到客戶端服務(wù)器
  6. 發(fā)出/resource請(qǐng)求到客戶端服務(wù)器
  7. 客戶端服務(wù)器將/resource請(qǐng)求轉(zhuǎn)發(fā)到Resource服務(wù)器
  8. Resource服務(wù)器要求安全驗(yàn)證,于是直接從授權(quán)服務(wù)器獲取認(rèn)證授權(quán)信息進(jìn)行判斷后(最后會(huì)響應(yīng)給客戶端服務(wù)器,客戶端服務(wù)器再響應(yīng)給瀏覽中器)

SSO 角色

  1. 統(tǒng)一認(rèn)證服務(wù) AuthorizationServer
  2. SSO 客戶端 OAuth2Sso

工程結(jié)構(gòu)

image.png

認(rèn)證服務(wù)實(shí)現(xiàn)

工程結(jié)構(gòu)

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   # 應(yīng)用名稱

  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

  # 數(shù)據(jù)源 配置
  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 登錄身份證認(rèn)證

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


}

權(quán)限認(rèn)證服務(wù)配置 AuthorizationServerConfiguration

/***
 *  身份授權(quán)認(rèn)證服務(wù)配置
 *  配置客戶端蝶缀、token存儲(chǔ)方式等
 */
@Configuration
@EnableAuthorizationServer  //  注解開啟驗(yàn)證服務(wù)器 提供/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";   // 密碼模式授權(quán)模式
    private static final String AUTHORIZATION_CODE = "authorization_code"; //授權(quán)碼模式  授權(quán)碼模式使用到了回調(diào)地址,是最為復(fù)雜的方式算灸,通常網(wǎng)站中經(jīng)常出現(xiàn)的微博扼劈,qq第三方登錄,都會(huì)采用這個(gè)形式菲驴。
    private static final String REFRESH_TOKEN = "refresh_token";  //
    private static final String IMPLICIT = "implicit"; //簡(jiǎn)化授權(quán)模式
    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";    //指定哪些資源是需要授權(quán)驗(yàn)證的


    @Autowired
    private AuthenticationManager authenticationManager;   //認(rèn)證方式
    @Resource(name = "userService")
    private UserDetailsService userDetailsService;


    @Override
    public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
        String secret = new BCryptPasswordEncoder().encode(CLIENT_SECRET);  // 用 BCrypt 對(duì)密碼編碼
        //配置3個(gè)個(gè)客戶端,一個(gè)用于password認(rèn)證荐吵、一個(gè)用于client認(rèn)證、一個(gè)用于authorization_code認(rèn)證
        configurer.inMemory()  // 使用in-memory存儲(chǔ)
                .withClient(CLIEN_ID_THREE)    //client_id用來(lái)標(biāo)識(shí)客戶的Id  客戶端3
                .resourceIds(RESOURCE_ID)
                .authorizedGrantTypes(AUTHORIZATION_CODE,GRANT_TYPE, REFRESH_TOKEN,GRANT_TYPE_PASSWORD,IMPLICIT)  //允許授權(quán)類型
                .scopes(SCOPE_READ,SCOPE_WRITE,TRUST)  //允許授權(quán)范圍
                .authorities("ROLE_CLIENT")  //客戶端可以使用的權(quán)限
                .secret(secret)  //secret客戶端安全碼
                //.redirectUris(REDIRECT_URL)  //指定可以接受令牌和授權(quán)碼的重定向URIs
                .autoApprove(true) // 為true 則不會(huì)被重定向到授權(quán)的頁(yè)面赊瞬,也不需要手動(dòng)給請(qǐng)求授權(quán),直接自動(dòng)授權(quán)成功返回code
                .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)   //token 時(shí)間秒
                .refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS);//刷新token 時(shí)間 秒

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager)
                .accessTokenConverter(accessTokenConverter())
                .userDetailsService(userDetailsService) //必須注入userDetailsService否則根據(jù)refresh_token無(wú)法加載用戶信息
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST,HttpMethod.OPTIONS)  //支持GET  POST  請(qǐng)求獲取token
                .reuseRefreshTokens(true) //開啟刷新token
                .tokenServices(tokenServices());

    }


    /**
     * 認(rèn)證服務(wù)器的安全配置
     *
     * @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(); //允許表單認(rèn)證  這段代碼在授權(quán)碼模式下會(huì)導(dǎo)致無(wú)法根據(jù)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();
                //只有如下兩種模式才能獲取到當(dāng)前用戶信息
                if("authorization_code".equals(grantType) || "password".equals(grantType)) {
                    String userName = authentication.getUserAuthentication().getName();
                    // 自定義一些token 信息 會(huì)在獲取token返回結(jié)果中展示出來(lái)
                    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實(shí)現(xiàn)令牌(Access Token)
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * 重寫默認(rèn)的資源服務(wù)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;
    }

}

資源服務(wù)認(rèn)證配置 ResourceServerConfiguration


@Configuration
@EnableResourceServer   //注解來(lái)開啟資源服務(wù)器
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 {
       
        // 配置那些資源需要保護(hù)的
        http.requestMatchers().antMatchers("/api/**")
                .and()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .and()
                .exceptionHandling().accessDeniedHandler(customAccessDeniedHandler())  //權(quán)限認(rèn)證失敗業(yè)務(wù)處理
                .authenticationEntryPoint(customAuthenticationEntryPoint());  //認(rèn)證失敗的業(yè)務(wù)處理
        http.addFilterBefore(permitAuthenticationFilter,AbstractPreAuthenticatedProcessingFilter.class); //自定義token過濾 token校驗(yàn)失敗后自定義返回?cái)?shù)據(jù)格式
    
    }
    @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 驗(yàn)證失敗后自定義返回?cái)?shù)據(jù)格式
     * @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()){
                    //自定義返回?cái)?shù)據(jù)格式
                    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 {
        //解決靜態(tài)資源被攔截的問題
        web.ignoring().antMatchers("/assets/**");
        web.ignoring().antMatchers("/favicon.ico");

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .requestMatchers()   // requestMatchers 配置 數(shù)組
                .antMatchers("/oauth/**","/login","/home")
                .and()
                .authorizeRequests()         //authorizeRequests 配置權(quán)限 順序?yàn)橄扰渲眯枰判械膗rl 在配置需要權(quán)限的url先煎,最后再配置.anyRequest().authenticated()
                .antMatchers("/oauth/**").authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll();
        http.addFilterBefore(simpleCORSFilter, SecurityContextPersistenceFilter.class);
    }



    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

跨域設(shè)置 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);
    }
}

自定義過濾器驗(yàn)證token 返回自定義數(shù)據(jù)格式 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("當(dāng)前訪問的URL地址:" +request.getRequestURI());
      Authentication authentication = this.tokenExtractor.extract(request);
      if (authentication == null) {
          if (this.stateless && this.isAuthenticated()) {
             // SecurityContextHolder.clearContext();
          }
          log.info("當(dāng)前訪問的URL地址:" +request.getRequestURI() +"不進(jìn)行攔截...");
          filterChain.doFilter(request, response);
      } else {
          log.info("************ 開始驗(yàn)證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("** 無(wú)校的token信息. ** ");
                  // throw new AccessDeniedException("無(wú)校的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("** 無(wú)校的token信息. ** ");
                  // throw new AccessDeniedException("無(wú)校的token信息.");
              }

          }else {
              error.set(true);
              map.put("message","參數(shù)無(wú)token.");
              log.info("** 參數(shù)無(wú)token. ** ");
              //throw new AccessDeniedException("參數(shù)無(wú)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);
  }
}

頁(yè)面跳轉(zhuǎn)url注冊(cè) MvcConfig


@Configuration
public class MvcConfig implements  WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/login").setViewName("login"); //自定義的登陸頁(yè)面
        registry.addViewController("/oauth/confirm_access").setViewName("oauth_approval"); //自定義的授權(quán)頁(yè)面
        registry.addViewController("/oauth_error").setViewName("oauth_error");
    }


}

自定義身份證認(rèn)證失敗返回?cái)?shù)據(jù)格式 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("請(qǐng)求url:" +httpServletRequest.getRequestURI());
        log.info("  ============ 身份認(rèn)證失敗..................... ");
        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);
    }

}

測(cè)試 Controller UserController

@CrossOrigin
@RestController
public class UserController {

    @GetMapping("oauth/me")
    public Principal getUser(Principal user){
        System.out.println(".. 進(jìn)入 獲取用戶信息 方法   ..........  ");
        System.out.println(JSON.toJSONString(user));
        return user;
    }

    @GetMapping("api/user")
    public Principal user(Principal user){
        System.out.println(".. 進(jìn)入 獲取用戶信息 方法   ..........  ");
        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("俏如來(lái)");
        list.add("帝如來(lái)");
        list.add("鬼如來(lái)");
        return list;
    }

    @RequestMapping(path = "api/messages", method = RequestMethod.POST)
   public String postMessage(Principal principal) {
        return "POST -> 默蒼離 ";
    }

    /**
     * 當(dāng)前登錄人信息
     * @return
     */
    @RequestMapping(path = "api/loginUser", method = RequestMethod.GET)
    public UserDetails currentlyLoginUser(){
         UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
         return  userDetails;
    }


}

把字符串?dāng)?shù)據(jù)輸出到頁(yè)面 ResultUtil

public class ResultUtil {

    /**
     * 將json輸出到前端(參數(shù)非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");
        /* 設(shè)置瀏覽器跨域訪問 */
        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();
        }
    }
}

啟動(dòng)類 SecurityOauth2AuthorizationServerApplication


@SpringBootApplication
public class SecurityOauth2AuthorizationServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityOauth2AuthorizationServerApplication.class, args);
    }
}

登錄html頁(yè)面 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

首頁(yè) html 頁(yè)面 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">
                                                           點(diǎn)擊進(jìn)入<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">
                                                            點(diǎn)擊進(jìn)入<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">
                                                            點(diǎn)擊進(jìn)入<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 客戶端

項(xiàng)目結(jié)構(gòu)

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   # 應(yīng)用名稱

  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

  # 數(shù)據(jù)源 配置
  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 認(rèn)證配置
oauth2-server: http://localhost:18082

security:
  oauth2:
    client:
     # grant-type: client_credentials    # 授權(quán)模式
      client-id: client_3        # 在oauth 服務(wù)端注冊(cè)的client-id
      client-secret: secret     # 在oauth 服務(wù)端注冊(cè)的secret
      access-token-uri: ${oauth2-server}/oauth/token    #獲取token 地址
      user-authorization-uri: ${oauth2-server}/oauth/authorize  # 認(rèn)證地址
      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注解來(lái)開啟SSO
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private SimpleCORSFilter simpleCORSFilter;

    @Value("${oauth2-server}")
    private String oauthServerUrl;

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解決靜態(tài)資源被攔截的問題
        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   //注解來(lái)開啟資源服務(wù)器
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);

        // 配置那些資源需要保護(hù)的
        http.csrf().disable()
                .requestMatchers().antMatchers("/api/**")
                .and()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .and()
                .exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
        http.addFilterBefore(oAuth2AuthenticationFilter,AbstractPreAuthenticatedProcessingFilter.class); // 這種方式也可以達(dá)到token校驗(yàn)失敗后自定義返回?cái)?shù)據(jù)格式  使用此方式需要將上面的代碼啟用
    }

    /**
     * 重寫 token 驗(yàn)證失敗后自定義返回?cái)?shù)據(jù)格式
     * @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()){
                    //自定義返回?cái)?shù)據(jù)格式
                    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() {}

}

頁(yè)面跳轉(zhuǎn)url 注冊(cè) 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();
    }
}

測(cè)試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","測(cè)試權(quán)限信息成功");
        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(".. 進(jìn)入 獲取用戶信息 方法   ..........  ");
        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);
    }
}

啟動(dòng)類 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 頁(yè)面 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  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">
                    請(qǐng)求訪問其他客戶端資源 
                    <i class="icon-arrow-right"></i>
                </span>
                <span class="ladda-spinner"></span>
            </button>
        </div>
    </div>

    <div class="row">
        <div class="col-sm-12">
            <h1>獲取其他服務(wù)登錄人接口信息</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服務(wù)的當(dāng)前登錄人信息 
                    <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服務(wù)的登錄人信息</h1>
            <div id="user_info"></div>
        </div>
    </div>


    <div class="row">
        <div class="col-sm-12">
            <h1>獲取自身服務(wù)登錄人接口信息</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服務(wù)的當(dāng)前登錄人信息 
                    <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服務(wù)的登錄人信息</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服務(wù)的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. 請(qǐng)求授權(quán)訪問18083端口應(yīng)用服務(wù)

http://localhost:18082/oauth/authorize?response_type=code&client_id=client_3&redirect_uri=http://localhost:18083/index
如果處于未登錄狀態(tài)則會(huì)跳轉(zhuǎn)到認(rèn)證服務(wù)器的登錄頁(yè)面

image.png

2. 登錄成功后回跳到http://localhost:18083/index 頁(yè)面 并且攜帶code值

image.png

3. 根據(jù)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("請(qǐng)求獲取token出現(xiàn)錯(cuò)誤.");
            }
        });

4. 攜帶token 訪問認(rèn)證服務(wù)端的資源接口

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

返回?cái)?shù)據(jù):


image.png

5. 訪問授權(quán)18082端口應(yīng)用

window.open("http://localhost:18082/oauth/authorize?response_type=code&client_id=client_3&redirect_uri=http://localhost:18082/home");

如果已經(jīng)處于登錄狀態(tài) 則直接進(jìn)入http://localhost:18082/home頁(yè)面

image.png

6. 攜帶token 訪問18083端口服務(wù)接口資源

       $.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("請(qǐng)求獲取localhost:18083/api/user服務(wù)登錄人信息接口出錯(cuò).");
                }
            });
        })

返回?cái)?shù)據(jù)


image.png

7. 當(dāng)請(qǐng)求訪問資源未攜帶token 認(rèn)證服務(wù)會(huì)進(jìn)入 CustomAuthenticationEntryPoint 類中

image.png
image.png

8. 當(dāng)前請(qǐng)求攜帶錯(cuò)誤的token 會(huì)在 PermitAuthenticationFilter 驗(yàn)證處理

image.png

image.png

8. 未登錄狀態(tài)下訪問sso client 客戶端資源接口或者認(rèn)證服務(wù)端的資源接口時(shí) 都會(huì)跳轉(zhuǎn)到登錄頁(yè)面去

image.png

image.png

image.png

demo地址:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末巧涧,一起剝皮案震驚了整個(gè)濱河市薯蝎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谤绳,老刑警劉巖占锯,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異缩筛,居然都是意外死亡消略,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門瞎抛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)艺演,“玉大人,你說(shuō)我怎么就攤上這事√コ罚” “怎么了晓殊?”我有些...
    開封第一講書人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)伤提。 經(jīng)常有香客問我巫俺,道長(zhǎng),這世上最難降的妖魔是什么肿男? 我笑而不...
    開封第一講書人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任识藤,我火速辦了婚禮,結(jié)果婚禮上次伶,老公的妹妹穿的比我還像新娘。我一直安慰自己稽穆,他們只是感情好冠王,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舌镶,像睡著了一般柱彻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上餐胀,一...
    開封第一講書人閱讀 50,084評(píng)論 1 291
  • 那天哟楷,我揣著相機(jī)與錄音,去河邊找鬼否灾。 笑死卖擅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的墨技。 我是一名探鬼主播惩阶,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼扣汪!你這毒婦竟也來(lái)了断楷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤崭别,失蹤者是張志新(化名)和其女友劉穎冬筒,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體茅主,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡舞痰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了暗膜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匀奏。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖学搜,靈堂內(nèi)的尸體忽然破棺而出娃善,到底是詐尸還是另有隱情论衍,我是刑警寧澤,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布聚磺,位于F島的核電站坯台,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瘫寝。R本人自食惡果不足惜蜒蕾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望焕阿。 院中可真熱鬧咪啡,春花似錦、人聲如沸暮屡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)褒纲。三九已至准夷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間莺掠,已是汗流浹背衫嵌。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留彻秆,地道東北人楔绞。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像掖棉,于是被迫代替她去往敵國(guó)和親墓律。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容