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

工程结构

Spring boot 2.0 整合 oauth2 SSO_第1张图片
image.png

认证服务实现

工程结构

Spring boot 2.0 整合 oauth2 SSO_第2张图片
image.png

pom.xml

  
        UTF-8
        1.0.9.RELEASE
        0.9.0
    

    
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.cloud
            spring-cloud-starter-oauth2
        
        
            org.springframework.cloud
            spring-cloud-starter-security
        
        
            org.springframework.security
            spring-security-jwt
            ${security-jwt.version}
        

        
            io.jsonwebtoken
            jjwt
            ${jjwt.version}
        
        
            com.alibaba
            fastjson
            1.2.47
        
        
            org.postgresql
            postgresql
            runtime
        
        
            org.springframework.boot
            spring-boot-devtools
            true
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        
        
            org.springframework.boot
            spring-boot-starter-freemarker
        
        
            org.apache.commons
            commons-lang3
            3.7
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

application.yml

server:
  port: 18082

spring:
  application:
    name: oauth2-server   # 应用名称

  jpa:
      open-in-view: true
      database: POSTGRESQL
      show-sql: true
      hibernate:
        ddl-auto: update
        dialect: org.hibernate.dialect.PostgreSQLDialect
      properties:
        hibernate:
          temp:
            use_jdbc_metadata_defaults: false

  # 数据源 配置
  datasource:
      platform: postgres
      url: jdbc:postgresql://127.0.0.1:5432/cloud_oauth2?useUnicode=true&characterEncoding=utf-8
      username: postgres
      password: postgres123
      driver-class-name: org.postgresql.Driver

  redis:
    host: 127.0.0.1
    database: 0

  thymeleaf:
      prefix: classpath:/static/pages/

# 不需要拦截的url地址
mySecurity:
  exclude:
    antMatchers: /oauth/**,/login,/home

logging:
  level:
    org.springframework.security: DEBUG

Security 登录身份证认证

@Slf4j
@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private SysAccountRepository repository;

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysAccount user = repository.findByUserAccount(username);
        if(user == null){
            log.info("登录用户【"+username + "】不存在.");
            throw new UsernameNotFoundException("登录用户【"+username + "】不存在.");
        }
        return new org.springframework.security.core.userdetails.User(user.getUserAccount(), user.getUserPwd(), getAuthority());
    }

    private List getAuthority() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }


}

权限认证服务配置 AuthorizationServerConfiguration

/***
 *  身份授权认证服务配置
 *  配置客户端、token存储方式等
 */
@Configuration
@EnableAuthorizationServer  //  注解开启验证服务器 提供/oauth/authorize,/oauth/token,/oauth/check_token,/oauth/confirm_access,/oauth/error
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    private static final String REDIRECT_URL = "https://www.baidu.com/";
    private static final String CLIEN_ID_THREE = "client_3";  //客户端3
    private static final String CLIENT_SECRET = "secret";   //secret客户端安全码
    private static final String GRANT_TYPE_PASSWORD = "password";   // 密码模式授权模式
    private static final String AUTHORIZATION_CODE = "authorization_code"; //授权码模式  授权码模式使用到了回调地址,是最为复杂的方式,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。
    private static final String REFRESH_TOKEN = "refresh_token";  //
    private static final String IMPLICIT = "implicit"; //简化授权模式
    private static final String GRANT_TYPE = "client_credentials";  //客户端模式
    private static final String SCOPE_READ = "read";
    private static final String SCOPE_WRITE = "write";
    private static final String TRUST = "trust";
    private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1*60*60;          //
    private static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60;        //
    private static final String RESOURCE_ID = "resource_id";    //指定哪些资源是需要授权验证的


    @Autowired
    private AuthenticationManager authenticationManager;   //认证方式
    @Resource(name = "userService")
    private UserDetailsService userDetailsService;


    @Override
    public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
        String secret = new BCryptPasswordEncoder().encode(CLIENT_SECRET);  // 用 BCrypt 对密码编码
        //配置3个个客户端,一个用于password认证、一个用于client认证、一个用于authorization_code认证
        configurer.inMemory()  // 使用in-memory存储
                .withClient(CLIEN_ID_THREE)    //client_id用来标识客户的Id  客户端3
                .resourceIds(RESOURCE_ID)
                .authorizedGrantTypes(AUTHORIZATION_CODE,GRANT_TYPE, REFRESH_TOKEN,GRANT_TYPE_PASSWORD,IMPLICIT)  //允许授权类型
                .scopes(SCOPE_READ,SCOPE_WRITE,TRUST)  //允许授权范围
                .authorities("ROLE_CLIENT")  //客户端可以使用的权限
                .secret(secret)  //secret客户端安全码
                //.redirectUris(REDIRECT_URL)  //指定可以接受令牌和授权码的重定向URIs
                .autoApprove(true) // 为true 则不会被重定向到授权的页面,也不需要手动给请求授权,直接自动授权成功返回code
                .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)   //token 时间秒
                .refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS);//刷新token 时间 秒

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager)
                .accessTokenConverter(accessTokenConverter())
                .userDetailsService(userDetailsService) //必须注入userDetailsService否则根据refresh_token无法加载用户信息
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST,HttpMethod.OPTIONS)  //支持GET  POST  请求获取token
                .reuseRefreshTokens(true) //开启刷新token
                .tokenServices(tokenServices());

    }


    /**
     * 认证服务器的安全配置
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .realm(RESOURCE_ID)
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()") //isAuthenticated():排除anonymous   isFullyAuthenticated():排除anonymous以及remember-me
                .allowFormAuthenticationForClients(); //允许表单认证  这段代码在授权码模式下会导致无法根据code 获取token 
    }




    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
            /**
             * 自定义一些token返回的信息
             * @param accessToken
             * @param authentication
             * @return
             */
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                String grantType = authentication.getOAuth2Request().getGrantType();
                //只有如下两种模式才能获取到当前用户信息
                if("authorization_code".equals(grantType) || "password".equals(grantType)) {
                    String userName = authentication.getUserAuthentication().getName();
                    // 自定义一些token 信息 会在获取token返回结果中展示出来
                    final Map additionalInformation = new HashMap<>();
                    additionalInformation.put("user_name", userName);
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                }
                OAuth2AccessToken token = super.enhance(accessToken, authentication);
                return token;
            }
        };
        converter.setSigningKey("bcrypt");
        return converter;
    }


    @Bean
    public TokenStore tokenStore() {
        //基于jwt实现令牌(Access Token)
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * 重写默认的资源服务token
     * @return
     */
    @Bean
    public DefaultTokenServices tokenServices() {
        final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenEnhancer(accessTokenConverter());
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)); // 30天
        return defaultTokenServices;
    }

}

资源服务认证配置 ResourceServerConfiguration


@Configuration
@EnableResourceServer   //注解来开启资源服务器
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {



    private static final String RESOURCE_ID = "resource_id";
    @Autowired
    private DefaultTokenServices tokenServices;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private PermitAuthenticationFilter permitAuthenticationFilter;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID).stateless(true).tokenServices(tokenServices);
    }



    @Override
    public void configure(HttpSecurity http) throws Exception {
       
        // 配置那些资源需要保护的
        http.requestMatchers().antMatchers("/api/**")
                .and()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .and()
                .exceptionHandling().accessDeniedHandler(customAccessDeniedHandler())  //权限认证失败业务处理
                .authenticationEntryPoint(customAuthenticationEntryPoint());  //认证失败的业务处理
        http.addFilterBefore(permitAuthenticationFilter,AbstractPreAuthenticatedProcessingFilter.class); //自定义token过滤 token校验失败后自定义返回数据格式
    
    }
    @Bean
    public LogoutSuccessHandler customLogoutSuccessHandler(){
        return new CustomLogoutSuccessHandler();
    }


    @Bean
    public AuthenticationFailureHandler customLoginFailHandler(){
        return new CustomLoginFailHandler();
    }


    @Bean
    public OAuth2AuthenticationEntryPoint customAuthenticationEntryPoint(){
        return new CustomAuthenticationEntryPoint();
    }

    @Bean
    public OAuth2AccessDeniedHandler customAccessDeniedHandler(){
        return new CustomAccessDenieHandler();
    }


    /**
     * 重写 token 验证失败后自定义返回数据格式
     * @return
     */
    @Bean
    public WebResponseExceptionTranslator webResponseExceptionTranslator() {
        return new DefaultWebResponseExceptionTranslator() {
            @Override
            public ResponseEntity translate(Exception e) throws Exception {
                ResponseEntity responseEntity = super.translate(e);
                OAuth2Exception body = (OAuth2Exception) responseEntity.getBody();
                HttpHeaders headers = new HttpHeaders();
                headers.setAll(responseEntity.getHeaders().toSingleValueMap());
                // do something with header or response
                if(401==responseEntity.getStatusCode().value()){
                    //自定义返回数据格式
                    Map map =  new HashMap<>();
                    map.put("status","401");
                    map.put("message",e.getMessage());
                    map.put("timestamp", String.valueOf(LocalDateTime.now()));
                    return new ResponseEntity(JSON.toJSONString(map), headers, responseEntity.getStatusCode());
                }else{
                    return new ResponseEntity(body, headers, responseEntity.getStatusCode());
                }
            }
        };
    }

}

web安全配置 SecurityConfiguration

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableAutoConfiguration(exclude = {
        org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class })
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private SimpleCORSFilter simpleCORSFilter;

    @Resource(name = "userService")
    private UserDetailsService userDetailsService;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(bCryptPasswordEncoder());
        auth.parentAuthenticationManager(authenticationManagerBean());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/assets/**");
        web.ignoring().antMatchers("/favicon.ico");

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .requestMatchers()   // requestMatchers 配置 数组
                .antMatchers("/oauth/**","/login","/home")
                .and()
                .authorizeRequests()         //authorizeRequests 配置权限 顺序为先配置需要放行的url 在配置需要权限的url,最后再配置.anyRequest().authenticated()
                .antMatchers("/oauth/**").authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll();
        http.addFilterBefore(simpleCORSFilter, SecurityContextPersistenceFilter.class);
    }



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

}

跨域设置 SimpleCORSFilter

@Slf4j
@Component
public class SimpleCORSFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        httpServletRequest.setCharacterEncoding("utf-8");
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setHeader("Content-Type", "application/json");
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");//允许所以域名访问,
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");//允许的访问方式
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with,content-type,Authorization");
        httpServletResponse.setHeader("Access-Control-Request-Headers", "x-requested-with,content-type,Accept,Authorization");
        httpServletResponse.setHeader("Access-Control-Request-Method", "GET,POST,PUT,DELETE,OPTIONS");
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

自定义过滤器验证token 返回自定义数据格式 PermitAuthenticationFilter

@Slf4j
@Component
public class PermitAuthenticationFilter extends OAuth2AuthenticationProcessingFilter {

  private static final String BEARER_AUTHENTICATION = "Bearer ";
  private static final String HEADER_AUTHORIZATION = "authorization";
  private TokenExtractor tokenExtractor = new BearerTokenExtractor();
  private boolean stateless = true;
  OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
  @Autowired
  private TokenStore tokenStore;


  public PermitAuthenticationFilter() {
      DefaultTokenServices dt = new DefaultTokenServices();
      dt.setTokenStore(tokenStore);
      oAuth2AuthenticationManager.setTokenServices(dt);
      this.setAuthenticationManager(oAuth2AuthenticationManager);
  }

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {

  }

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
      HttpServletResponse response = (HttpServletResponse) servletResponse;
      HttpServletRequest request = (HttpServletRequest)servletRequest;
      log.info(" ================== =========================== ===================");
      log.info("当前访问的URL地址:" +request.getRequestURI());
      Authentication authentication = this.tokenExtractor.extract(request);
      if (authentication == null) {
          if (this.stateless && this.isAuthenticated()) {
             // SecurityContextHolder.clearContext();
          }
          log.info("当前访问的URL地址:" +request.getRequestURI() +"不进行拦截...");
          filterChain.doFilter(request, response);
      } else {
          log.info("************ 开始验证token ..........................   ");
          String accessToken = request.getParameter("access_token");
          String headerToken = request.getHeader(HEADER_AUTHORIZATION);
          Map map =  new HashMap<>();
          map.put("status","403");
          AtomicBoolean error = new AtomicBoolean(false);
          if(StringUtils.isNotBlank(accessToken)){
              try {
                  OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
                  log.info("token =" +oAuth2AccessToken.getValue());
              }catch (InvalidTokenException e){
                  error.set(true);
                  map.put("message",e.getMessage());
                  log.info("** 无校的token信息. ** ");
                  // throw new AccessDeniedException("无校的token信息.");
              }

          }else if (StringUtils.isNotBlank(headerToken) && headerToken.startsWith(BEARER_AUTHENTICATION)){
              try {
                  OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(headerToken.split(" ")[0]);
                  log.info("token =" +oAuth2AccessToken.getValue());
              }catch (InvalidTokenException e){
                  error.set(true);
                  map.put("message",e.getMessage());
                  log.info("** 无校的token信息. ** ");
                  // throw new AccessDeniedException("无校的token信息.");
              }

          }else {
              error.set(true);
              map.put("message","参数无token.");
              log.info("** 参数无token. ** ");
              //throw new AccessDeniedException("参数无token.");
          }
          if (!error.get()){
              filterChain.doFilter(request, response);
          }else {
              map.put("path", request.getServletPath());
              map.put("timestamp", String.valueOf(LocalDateTime.now()));
              response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
              ResultUtil.writeJavaScript(response,map);
          }
      }
  }

  @Override
  public void destroy() {

  }

  private boolean isAuthenticated() {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      return authentication != null && !(authentication instanceof AnonymousAuthenticationToken);
  }
}

页面跳转url注册 MvcConfig


@Configuration
public class MvcConfig implements  WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/login").setViewName("login"); //自定义的登陆页面
        registry.addViewController("/oauth/confirm_access").setViewName("oauth_approval"); //自定义的授权页面
        registry.addViewController("/oauth_error").setViewName("oauth_error");
    }


}

自定义身份证认证失败返回数据格式 CustomAuthenticationEntryPoint

@Slf4j
@Component
public class CustomAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        log.info(" ====================================================== ");
        log.info("请求url:" +httpServletRequest.getRequestURI());
        log.info("  ============ 身份认证失败..................... ");
        log.info(e.getMessage());
        log.info(e.getLocalizedMessage());
        e.printStackTrace();
        Map map =  new HashMap<>();
        map.put("status","401");
        map.put("message",e.getMessage());
        map.put("path", httpServletRequest.getServletPath());
        map.put("timestamp", String.valueOf(LocalDateTime.now()));
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        ResultUtil.writeJavaScript(httpServletResponse,map);
    }

}

测试 Controller UserController

@CrossOrigin
@RestController
public class UserController {

    @GetMapping("oauth/me")
    public Principal getUser(Principal user){
        System.out.println(".. 进入 获取用户信息 方法   ..........  ");
        System.out.println(JSON.toJSONString(user));
        return user;
    }

    @GetMapping("api/user")
    public Principal user(Principal user){
        System.out.println(".. 进入 获取用户信息 方法   ..........  ");
        System.out.println(JSON.toJSONString(user));
        return user;
    }




    @RequestMapping(path = "api/messages", method = RequestMethod.GET)
    public List getMessages(Principal principal) {
        List list = new LinkedList<>();
        list.add("俏如来");
        list.add("帝如来");
        list.add("鬼如来");
        return list;
    }

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

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


}

把字符串数据输出到页面 ResultUtil

public class ResultUtil {

    /**
     * 将json输出到前端(参数非json格式)
     * @param response
     * @param obj  任意类型
     */
    public static void writeJavaScript(HttpServletResponse response, Object obj){
        response.setContentType("application/json;charset=UTF-8");
        response.setHeader("Cache-Control","no-store, max-age=0, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        /* 设置浏览器跨域访问 */
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,Authorization");
        response.setHeader("Access-Control-Allow-Credentials","true");
        try {
            PrintWriter out = response.getWriter();
            out.write(JSON.toJSONString(obj));
            out.flush();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

启动类 SecurityOauth2AuthorizationServerApplication


@SpringBootApplication
public class SecurityOauth2AuthorizationServerApplication {

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

登录html页面 login.html




    
    OAuth2 SSO login
    
    
    
    
    
    
    




login.html 效果图:


Spring boot 2.0 整合 oauth2 SSO_第3张图片
image.png

首页 html 页面 home.html




    
    OAuth2 SSO login
    
    
    
    
    
    
    
    
    
    




    







    


    



home.html 效果图


Spring boot 2.0 整合 oauth2 SSO_第4张图片
image.png

SSO client 客户端

项目结构

Spring boot 2.0 整合 oauth2 SSO_第5张图片
image.png

pom.xml

  
        UTF-8
        1.0.9.RELEASE
        0.9.0
    

    
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        
        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.cloud
            spring-cloud-starter-oauth2
        
        
            org.springframework.cloud
            spring-cloud-starter-security
        
        
            org.springframework.security
            spring-security-jwt
            ${security-jwt.version}
        
        
            com.alibaba
            fastjson
            1.2.47
        
        
            io.jsonwebtoken
            jjwt
            ${jjwt.version}
        
        
            org.postgresql
            postgresql
            runtime
        
        
            org.springframework.boot
            spring-boot-devtools
            true
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

application.yml

server:
  port: 18083


spring:
  application:
    name: oauth2-sso-client1   # 应用名称

  jpa:
      open-in-view: true
      database: POSTGRESQL
      show-sql: true
      hibernate:
        ddl-auto: update
        dialect: org.hibernate.dialect.PostgreSQLDialect
      properties:
        hibernate:
          temp:
            use_jdbc_metadata_defaults: false

  # 数据源 配置
  datasource:
      platform: postgres
      url: jdbc:postgresql://127.0.0.1:5432/cloud_oauth2?useUnicode=true&characterEncoding=utf-8
      username: postgres
      password: postgres123
      driver-class-name: org.postgresql.Driver

  redis:
    host: 127.0.0.1
    database: 0

  thymeleaf:
      prefix: classpath:/static/pages/
      #cache: false

  security:
    user:
      name: user
      password: e94a652b-adfb-4af7-ba00-d88419289172


# sso 认证配置
oauth2-server: http://localhost:18082

security:
  oauth2:
    client:
     # grant-type: client_credentials    # 授权模式
      client-id: client_3        # 在oauth 服务端注册的client-id
      client-secret: secret     # 在oauth 服务端注册的secret
      access-token-uri: ${oauth2-server}/oauth/token    #获取token 地址
      user-authorization-uri: ${oauth2-server}/oauth/authorize  # 认证地址
      scope: read,write
    resource:
      token-info-uri: ${oauth2-server}/oauth/check_token  # 检查token
      user-info-uri: ${oauth2-server}/oauth/me   # 用户信息
      jwt:
        key-uri: ${oauth2-server}/oauth/token_key
    sso:
      login-path: /login   



logging:
  level:
    org.springframework.security: DEBUG

web 安全配置 SecurityConfiguration

@Configuration
@EnableWebSecurity
@EnableOAuth2Sso  //@EnableOAuth2Sso注解来开启SSO
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private SimpleCORSFilter simpleCORSFilter;

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/assets/**");
        web.ignoring().antMatchers("/favicon.ico");

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .requestMatchers()
                .antMatchers("/oauth/**","/login","/index")
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/**").authenticated()
                .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler())
                .and()
                .formLogin()
                .permitAll()
                .loginProcessingUrl("/index");
        http.addFilterBefore(simpleCORSFilter,SecurityContextPersistenceFilter.class);
    }
}

资源配置 ResourceConfiguration


@Configuration
@EnableResourceServer   //注解来开启资源服务器
public class ResourceConfiguration  extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "resource_id";
    @Autowired
    private DefaultTokenServices tokenServices;
    @Autowired
    private TokenStore tokenStore;


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID).stateless(true).tokenServices(tokenServices);
    }



    @Override
    public void configure(HttpSecurity http) throws Exception {

        //如果 启用 http.addFilterBefore(oAuth2AuthenticationFilter,AbstractPreAuthenticatedProcessingFilter.class) 代码 则需要启用下面被注释的代码
        OAuth2AuthenticationProcessingFilter oAuth2AuthenticationFilter = new OAuth2AuthenticationProcessingFilter();
        OAuth2AuthenticationEntryPoint oAuth2AuthenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
        oAuth2AuthenticationEntryPoint.setExceptionTranslator(webResponseExceptionTranslator());
        oAuth2AuthenticationFilter.setAuthenticationEntryPoint(oAuth2AuthenticationEntryPoint);
        OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore);
        oAuth2AuthenticationManager.setTokenServices(defaultTokenServices);
        oAuth2AuthenticationFilter.setAuthenticationManager(oAuth2AuthenticationManager);

        // 配置那些资源需要保护的
        http.csrf().disable()
                .requestMatchers().antMatchers("/api/**")
                .and()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .and()
                .exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
        http.addFilterBefore(oAuth2AuthenticationFilter,AbstractPreAuthenticatedProcessingFilter.class); // 这种方式也可以达到token校验失败后自定义返回数据格式  使用此方式需要将上面的代码启用
    }

    /**
     * 重写 token 验证失败后自定义返回数据格式
     * @return
     */
    @Bean
    public WebResponseExceptionTranslator webResponseExceptionTranslator() {
        return new DefaultWebResponseExceptionTranslator() {
            @Override
            public ResponseEntity translate(Exception e) throws Exception {
                ResponseEntity responseEntity = super.translate(e);
                OAuth2Exception body = (OAuth2Exception) responseEntity.getBody();
                HttpHeaders headers = new HttpHeaders();
                headers.setAll(responseEntity.getHeaders().toSingleValueMap());
                // do something with header or response
                if(401==responseEntity.getStatusCode().value()){
                    //自定义返回数据格式
                    Map map =  new HashMap<>();
                    map.put("status","401");
                    map.put("message",e.getMessage());
                    map.put("timestamp", String.valueOf(LocalDateTime.now()));
                    return new ResponseEntity(JSON.toJSONString(map), headers, responseEntity.getStatusCode());
                }else{
                    return new ResponseEntity(body, headers, responseEntity.getStatusCode());
                }
            }
        };
    }
}

跨域 SimpleCORSFilter

@Component
public class SimpleCORSFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        String token = req.getParameter("access_token");
        System.out.println(" token -- "+ token);
        if(!StringUtils.isEmpty(token)){
            TokenContextHolder.setToken(token);
        }
        HttpServletResponse response = (HttpServletResponse) res;
        response.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Type", "application/json");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,content-type,Accept,Authorization");
        chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void destroy() {}

}

页面跳转url 注册 MvcConfiguration

@Configuration
public class MvcConfiguration implements  WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/index").setViewName("index");
        registry.addViewController("/securedPage");
    }


}

token 存放 TokenContextHolder

public class TokenContextHolder {

    private static final ThreadLocal LOCAL_TOKEN = new ThreadLocal<>();

    public static void setToken(String value){
        LOCAL_TOKEN.set(value);
    }

    public static String getToken(){
        String token = LOCAL_TOKEN.get();
        clearToken();
        return token;
    }

    public static void clearToken(){
        LOCAL_TOKEN.remove();
    }
}

测试Controller HomeController


@CrossOrigin
@RestController
public class HomeController {

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

    @Autowired
    IMessageService messageService;

    @Autowired
    OAuth2RestTemplate oAuth2RestTemplate;


    @RequestMapping("/getMessages")
    public List getMessages(){
        List list = oAuth2RestTemplate.getForObject(serverUrl+"/api/messages",List.class);
        list.stream().forEach(item ->{
            System.out.println(item);
        });
        return list;
    }

    @RequestMapping("api/test")
    public String test(){
        Map map = new HashMap<>();
        map.put("code","0");
        map.put("msg","测试权限信息成功");
        System.out.println(JSON.toJSONString(map));
        return JSON.toJSONString(map);
    }

    @RequestMapping("/postMessages")
    public String postMessage(){
        String token = TokenContextHolder.getToken();
        String str = oAuth2RestTemplate.postForObject(serverUrl+"api/messages?access_token="+token,null,String.class);
        Map map = new HashMap<>();
        map.put("msg",str);
        System.out.println(JSON.toJSONString(map));
        return JSON.toJSONString(map);
    }

    @GetMapping("api/user")
    public String user(){
        System.out.println(".. 进入 获取用户信息 方法   ..........  ");
        String token = TokenContextHolder.getToken();
        String str = oAuth2RestTemplate.getForObject(serverUrl+"api/user?access_token="+token,String.class);
        System.out.println(JSON.toJSONString(str));
        return JSON.toJSONString(str);
    }
}

启动类 Oauth2SsoClient1Application


@SpringBootApplication
public class Oauth2SsoClient1Application {

    @Bean
    OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext, OAuth2ProtectedResourceDetails details) {
        return new OAuth2RestTemplate(details, oauth2ClientContext);
    }

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

}

html 页面 index.html




    
    OAuth2 SSO Demo
    
    
    
    
    
    



Spring Security SSO

Login

authorization_code

获取其他服务登录人接口信息

localhost:18082服务的登录人信息

获取自身服务登录人接口信息

localhost:18083服务的登录人信息

获取自身postMessages接口信息

index.html 效果图


Spring boot 2.0 整合 oauth2 SSO_第6张图片
image.png

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

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

Spring boot 2.0 整合 oauth2 SSO_第7张图片
image.png

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

Spring boot 2.0 整合 oauth2 SSO_第8张图片
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);
            }
        });

返回数据:


Spring boot 2.0 整合 oauth2 SSO_第9张图片
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页面

Spring boot 2.0 整合 oauth2 SSO_第10张图片
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服务登录人信息接口出错.");
                }
            });
        })

返回数据


Spring boot 2.0 整合 oauth2 SSO_第11张图片
image.png

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

Spring boot 2.0 整合 oauth2 SSO_第12张图片
image.png
Spring boot 2.0 整合 oauth2 SSO_第13张图片
image.png

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

Spring boot 2.0 整合 oauth2 SSO_第14张图片
image.png

image.png

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

image.png

Spring boot 2.0 整合 oauth2 SSO_第15张图片
image.png

image.png

demo地址:

你可能感兴趣的:(Spring boot 2.0 整合 oauth2 SSO)