springcloud集成springsecurity oauth2 实现服务统一认证,应该是很简单的教程了~

/ 后端 / 没有评论 / 366浏览

#重构项目,整理maven父子结构,改善认证及资源服务器配置,增加注释更清晰好理解~

#2019.11.4  新增自定义返回token数据;

#2019.11.5 新增自定义登陆及授权页面(没有前后端分离,其实这种小页面也没必要分离出去)

1.项目结构

服务名 端口号 备注
auth 8082 认证服务器
mechant 8081 资源服务器
zuul 80 网关(这版可用可不用)

2.省略各模块结构生成及eureka等配置~

3.配置认证服务器

(1) 首先配置springsecurity,其实他底层是很多filter组成,顺序是请求先到他这里进行校验,然后在到oauth2进行资源认证


/**
 * @author: gaoyang
 * @Description: 身份认证拦截
 */
@Order(1)
@Configuration
//注解权限拦截
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
<span class="hljs-meta">@Autowired</span>
UserDetailsServiceConfig userDetailsServiceConfig;

<span class="hljs-comment">//认证服务器需配合Security使用</span>
<span class="hljs-meta">@Bean</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> AuthenticationManager <span class="hljs-title function_">authenticationManagerBean</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">super</span>.authenticationManagerBean();
}

<span class="hljs-comment">//websecurity用户密码和认证服务器客户端密码都需要加密算法</span>
<span class="hljs-meta">@Bean</span>
<span class="hljs-keyword">public</span> BCryptPasswordEncoder <span class="hljs-title function_">passwordEncoder</span><span class="hljs-params">()</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">BCryptPasswordEncoder</span>();
}


<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">configure</span><span class="hljs-params">(AuthenticationManagerBuilder auth)</span> <span class="hljs-keyword">throws</span> Exception {
    <span class="hljs-comment">//验证用户权限</span>
    auth.userDetailsService(userDetailsServiceConfig);
    <span class="hljs-comment">//也可以在内存中创建用户并为密码加密</span>
    <span class="hljs-comment">// auth.inMemoryAuthentication()</span>
    <span class="hljs-comment">//         .withUser("user").password(passwordEncoder().encode("123")).roles("USER")</span>
    <span class="hljs-comment">//         .and()</span>
    <span class="hljs-comment">//         .withUser("admin").password(passwordEncoder().encode("123")).roles("ADMIN");</span>
}

<span class="hljs-comment">//uri权限拦截,生产可以设置为启动动态读取数据库,具体百度</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">configure</span><span class="hljs-params">(HttpSecurity http)</span> <span class="hljs-keyword">throws</span> Exception {
    http
            <span class="hljs-comment">//此处不要禁止formLogin,code模式测试需要开启表单登陆,并且/oauth/token不要放开或放入下面ignoring,因为获取token首先需要登陆状态</span>
            .formLogin()
            .and()
            .csrf().disable()

            .authorizeRequests().antMatchers(<span class="hljs-string">"/test"</span>).permitAll()
            .and()
            .authorizeRequests().anyRequest().authenticated();
}

<span class="hljs-comment">//设置不拦截资源服务器的认证请求</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">configure</span><span class="hljs-params">(WebSecurity web)</span> <span class="hljs-keyword">throws</span> Exception {
    web.ignoring().antMatchers(<span class="hljs-string">"/oauth/check_token"</span>);
}

}

(2)这里的UserDetailsServiceConfig就是去校验登陆用户,可以写测试使用内存或者数据库方式读取用户信息(我这里写死了账号为user,密码为123)

@Component
public class UserDetailsServiceConfig implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    //生产环境使用数据库进行验证
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (!username.equals("user")) {
            throw new AcceptPendingException();
        }
        return new User(username, passwordEncoder.encode("123"),
                AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

(3)配置认证服务器(详见注释)


/**
 * @author: gaoyang
 * @Description:认证服务器配置
 */
@Order(2)
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    UserDetailsServiceConfig myUserDetailsService;

    //为了测试客户端与凭证存储在内存(生产应该用数据库来存储,oauth有标准数据库模板)
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client1-code") // client_id
                .secret(bCryptPasswordEncoder.encode("123")) // client_secret
                .authorizedGrantTypes("authorization_code") // 该client允许的授权类型
                .scopes("app") // 允许的授权范围
                .redirectUris("https://www.baidu.com")
                .resourceIds("goods", "mechant")    //资源服务器id,需要与资源服务器对应

                .and()
                .withClient("client2-credentials")
                .secret(bCryptPasswordEncoder.encode("123"))
                .authorizedGrantTypes("client_credentials")
                .scopes("app")
                .resourceIds("goods", "mechant")

                .and()
                .withClient("client3-password")
                .secret(bCryptPasswordEncoder.encode("123"))
                .authorizedGrantTypes("password")
                .scopes("app")
                .resourceIds("mechant")

                .and()
                .withClient("client4-implicit")
                .authorizedGrantTypes("implicit")
                .scopes("app")
                .resourceIds("mechant");
    }

    //配置token仓库
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //authenticationManager配合password模式使用
        endpoints.authenticationManager(authenticationManager)
                //这里使用内存存储token,也可以使用redis和数据库
                .tokenStore(new InMemoryTokenStore());
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
        endpoints.tokenEnhancer(new TokenEnhancer() {
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
                //在返回token的时候可以加上一些自定义数据
                DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) oAuth2AccessToken;
                Map<String, Object> map = new LinkedHashMap<>();
                map.put("nickname", "测试姓名");
                token.setAdditionalInformation(map);
                return token;
            }
        });
    }

    //配置token状态查询
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //开启支持通过表单方式提交client_id和client_secret,否则请求时以basic auth方式,头信息传递Authorization发送请求
        security.allowFormAuthenticationForClients();
    }

    //以下数据库配置
    /**
     *
     *     @Bean
     *     @Primary
     *     @ConfigurationProperties(prefix = "spring.datasource")
     *     public DataSource dataSource() {
     *         // 配置数据源(注意,我使用的是 HikariCP 连接池),以上注解是指定数据源,否则会有冲突
     *         return DataSourceBuilder.create().build();
     *     }
     *
     *     @Bean
     *     public TokenStore tokenStore() {
     *         // 基于 JDBC 实现,令牌保存到数据
     *         return new JdbcTokenStore(dataSource());
     *     }
     *
     *     @Bean
     *     public ClientDetailsService jdbcClientDetails() {
     *         // 基于 JDBC 实现,需要事先在数据库配置客户端信息
     *         return new JdbcClientDetailsService(dataSource());
     *     }
     *
     *     @Override
     *     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
     *         // 设置令牌
     *         endpoints.tokenStore(tokenStore());
     *     }
     *
     *     @Override
     *     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
     *         // 读取客户端配置
     *         clients.withClientDetails(jdbcClientDetails());
     *     }
     *
     */
}

(4) 新增自定义返回认证服务器数据:(这里只做演示,没有合理封装)

@RestController
@RequestMapping("/oauth")
public class CustomResult {

    @Autowired
    private TokenEndpoint tokenEndpoint;

    @GetMapping("/token")
    public Object getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

        return this.result(principal,parameters);
    }

    @PostMapping("/token")
    public Object postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

        return this.result(principal,parameters);
    }

    public Object result(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        ResponseEntity<OAuth2AccessToken> accessToken = tokenEndpoint.getAccessToken(principal, parameters);
        OAuth2AccessToken body = accessToken.getBody();
        Map<String, Object> customMap = body.getAdditionalInformation();
        String value = body.getValue();
        OAuth2RefreshToken refreshToken = body.getRefreshToken();
        Set<String> scope = body.getScope();
        int expiresIn = body.getExpiresIn();
        customMap.put("token",value);
        customMap.put("scope",scope);
        customMap.put("expiresIn",expiresIn);
        customMap.put("refreshToken",refreshToken);
        Map map = new HashMap();
        map.put("code",0);
        map.put("msg","success");
        map.put("data",customMap);
        return map;
    }
}

(5)添加获取token错误返回:(注意,客户端信息错误这里是拦截不到的)

@RestControllerAdvice
public class RestControllerExceptionAdvice {

    //判断oauth异常,自定义返回数据
    @ExceptionHandler
    public Object exception(OAuth2Exception e){
        //if ("invalid_client".equals(errorCode)) {
        //            return new InvalidClientException(errorMessage);
        //        } else if ("unauthorized_client".equals(errorCode)) {
        //            return new UnauthorizedClientException(errorMessage);
        //        } else if ("invalid_grant".equals(errorCode)) {
        //            return new InvalidGrantException(errorMessage);
        //        } else if ("invalid_scope".equals(errorCode)) {
        //            return new InvalidScopeException(errorMessage);
        //        } else if ("invalid_token".equals(errorCode)) {
        //            return new InvalidTokenException(errorMessage);
        //        } else if ("invalid_request".equals(errorCode)) {
        //            return new InvalidRequestException(errorMessage);
        //        } else if ("redirect_uri_mismatch".equals(errorCode)) {
        //            return new RedirectMismatchException(errorMessage);
        //        } else if ("unsupported_grant_type".equals(errorCode)) {
        //            return new UnsupportedGrantTypeException(errorMessage);
        //        } else if ("unsupported_response_type".equals(errorCode)) {
        //            return new UnsupportedResponseTypeException(errorMessage);
        //        } else {
        //            return (OAuth2Exception)("access_denied".equals(errorCode) ? new UserDeniedAuthorizationException(errorMessage) : new OAuth2Exception(errorMessage));
        //        }
        return "获取token错误";
    }
}

(6)添加自定义登陆及授权页面:

@Controller
// 必须配置该作用域设置
@SessionAttributes("authorizationRequest")
public class Oauth2Controller {


    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @RequestMapping("/authentication/require")
    @ResponseBody
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public Map requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {

        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (null != savedRequest) {
            String targetUrl = savedRequest.getRedirectUrl();
            System.out.println("引发跳转的请求是:" + targetUrl);
            redirectStrategy.sendRedirect(request, response, "/ologin");
        }
        //如果访问的是接口资源
        return new HashMap() {{
            put("code", 401);
            put("msg", "访问的服务需要身份认证,请引导用户到登录页");
        }};
    }

    @RequestMapping("/ologin")
    public String oauthLogin(){
        return "oauthLogin";
    }

    //授权控制器
    @RequestMapping("/oauth/confirm_access")
    public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
        Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ?
                model.get("scopes") : request.getAttribute("scopes"));
        List<String> scopeList = new ArrayList<>();
        if (scopes != null) {
            scopeList.addAll(scopes.keySet());
        }
        model.put("scopeList", scopeList);
        return "oauthGrant";
    }
}

  //uri权限拦截,生产可以设置为启动动态读取数据库,具体百度
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //此处不要禁止formLogin,code模式测试需要开启表单登陆,并且/oauth/token不要放开或放入下面ignoring,因为获取token首先需要登陆状态
                .formLogin().loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .passwordParameter("password")
                .usernameParameter("username")
                .and()
                .csrf().disable()

                .authorizeRequests().antMatchers("/test","/authentication/require","/ologin").permitAll()
                .and()
                .authorizeRequests().anyRequest().authenticated();
    }

4.配置资源服务器

(1)配置

//配置资源服务器
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //设置资源服务器id,需要与认证服务器对应
        resources.resourceId("mechant");
        //当权限不足时返回
        resources.accessDeniedHandler((request, response, e) -> {
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.getWriter()
                    .write(objectMapper.writeValueAsString(Result.from("0001", "权限不足", null)));
        });
        //当token不正确时返回
        resources.authenticationEntryPoint((request, response, e) -> {
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.getWriter()
                    .write(objectMapper.writeValueAsString(Result.from("0002", "access_token错误", null)));
        });
    }
    
    //配置uri拦截策略 
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .httpBasic().disable()
                .exceptionHandling()
                .authenticationEntryPoint((req, resp, exception) -> {
                    resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                    resp.getWriter()
                            .write(objectMapper.writeValueAsString(Result.from("0002", "没有携带token", null)));
                })
                .and()
                //无需登陆
                .authorizeRequests().antMatchers("/noauth").permitAll()
                .and()
                //拦截所有请求,并且检查sope
                .authorizeRequests().anyRequest().access("isAuthenticated() && #oauth2.hasScope('app')");
    }

    //静态内部返回类
    @Data
    static class Result<T> {
        private String code;
        private String msg;
        private T data;

        public Result(String code, String msg, T data) {
            this.code = code;
            this.msg = msg;
            this.data = data;
        }

        public static <T> Result from(String code, String msg, T data) {
            return new Result(code, msg, data);
        }
    }
}

(2)测试接口

@RestController
public class TestController {

    @GetMapping("ping")
    public Object test() {
        return "pong";
    }
   
    //无需登陆
    @GetMapping("noauth")
    public Object noauth() {
        return "noauth";
    }

}

(3)application.yml配置(远程向认证服务器鉴权)

#配置向认证服务器认证权限
security:
  oauth2:
    client:
      client-id: client3-password
      client-secret: 123
      access-token-uri: http://localhost:8082/oauth/token
      user-authorization-uri: http://localhost:8082/oauth/authorize
    resource:
      token-info-uri: http://localhost:8082/oauth/check_token

5.测试用例~

(1)password模式

表单方式:(localhost:8082/oauth/token?username=user&password=123&grant_type=password&client_secret=123&client_id=client3-password)

注意需要开启认证服务器的:

 @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //开启支持通过表单方式提交client_id和client_secret,否则请求时以basic auth方式,头信息传递Authorization发送请求
        security.allowFormAuthenticationForClients();
    }

表单加token方式:

(2)code模式

浏览器访问:  localhost:8082/oauth/authorize?client_id=client1-code&response_type=code

跳转到登陆页面:

选择允许

然后跳转到之前设置的地址,并携带code:

拿着code请求token:

自定义登陆及授权页面:

## 当前这样配置的话,如果各微服务之间互相调用,则是没有权限的;所以我们可以给他加上token,例如使用feign:

@Component
public class OauthConfig  implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.query("access_token","71f422b5-2204-4653-8a85-9cf2c62aac81");
    }
}

这里我写固定了,其实实现的话可以在认证服务器指定一个客户端模式,然后去动态获取token,这个token的过期缓存等等,可以自由发挥;

其实现在很多微服务都是内网通信,通过路由暴露端口了,所以可以定制一些特殊请求,来做无权限访问?

其他几种简单方式这里不介绍了~后续打算继续完善,并且加入vue前端定制页面~

     gayhub地址  喜欢的点星星~