oauth_oauth2 - CSDN
精华内容
参与话题
  • oauth2的简单实现

    千次阅读 2019-04-12 09:27:30
    Oauth2的学习总结   这段时间领导叫我学习oauth2准备一下,以后上spring cloud的时候可以上手就使用。陆陆续续学习了两周,三周还是更多,发现oauth2还是很复杂的。这篇文章就是想总结一下这段时间学习oauth2的...

    Oauth2的学习总结

      这段时间领导叫我学习oauth2准备一下,以后上spring cloud的时候可以上手就使用。陆陆续续学习了两周,三周还是更多,发现oauth2还是很复杂的。这篇文章就是想总结一下这段时间学习oauth2的东西,希望可以给刚学习oauth2的做一些铺垫,有些地方不对的还希望指正。

    1.oauth2的简单介绍

      oauth2总体我的理解是做认证授权的,最常见的就是第三方认证(QQ,微信,淘宝等),还有就是微服务的时候做认证使用。具体的概念,可以详见oauth2的解释,详细的oauth2的原理可以看大佬的博客

    2.oauth2的四种模式

    1.简单模式(implicit)
     一般不使用,完全不可信
    2.客户端模式(client credentials)
     客户端是可信的,只要传入一些参数就能获取access token
    3.密码模式(authorizaiton code)
     需要提供用户名和密码去实现获取access token
    4.code模式(password)
     完整的模式,先获取code,然后通过code去获取access token

    3.具体实现

    3.1 本文实现的逻辑
    客户端(浏览器)认证客户端资源服务器1.获取access token2.发送消息(带access token的head头)3.认证access token4.验证token的权限,获取内容客户端(浏览器)认证客户端资源服务器
    3.2 代码逻辑的实现
    3.2.1 认证中心实现

      AuthorizationServerConfigurerAdapter这个是用来设定认证中心相关配置

    @Configuration
    @EnableAuthorizationServer
    public class OAuth2Configure extends AuthorizationServerConfigurerAdapter {
        @Autowired
        @Qualifier("authenticationManagerBean")
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
    
        @Autowired
        private MyUserDetailsService myUserDetailsService;
    
        @Bean
        public MyRedisTokenStore tokenStore(){
            return new MyRedisTokenStore(redisConnectionFactory);
        }
    
        //client模式的设定
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
            clients.inMemory()
                    .withClient("user-service")
                    .secret("{noop}123456")    //{noop}代表的是明文,这个和加密模式相关
                    .scopes("service")
                    .authorizedGrantTypes("refresh_token","password","client_credentials")
                    .accessTokenValiditySeconds(60)
                    .refreshTokenValiditySeconds(6000)
            .and()
                    .withClient("client-service")
                    .secret("{noop}123456")
                    .scopes("client")
                    .authorizedGrantTypes("refresh_token","client_credentials")
                    .accessTokenValiditySeconds(60)
                    .refreshTokenValiditySeconds(6000);
        }
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
            //设定userDetailsService,若无,refresh_token会有UserDetailsService is required错误
            endpoints.authenticationManager(authenticationManager).userDetailsService(myUserDetailsService).tokenStore(tokenStore());
        }
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security
                    .allowFormAuthenticationForClients()
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("permitAll()");
    //                .checkTokenAccess("isAuthenticated()");
        }
    }
    
    

      重写redis,如果调用原先的有的RedisTokenStore的话会报错,会报get方法没有啥的(之前查到以后忘记掉错误是啥了)

    @Component
    public class MyRedisTokenStore implements TokenStore {
        private static final String ACCESS = "access:";
        private static final String AUTH_TO_ACCESS = "auth_to_access:";
        private static final String AUTH = "auth:";
        private static final String REFRESH_AUTH = "refresh_auth:";
        private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
        private static final String REFRESH = "refresh:";
        private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
        private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
        private static final String UNAME_TO_ACCESS = "uname_to_access:";
        private final RedisConnectionFactory connectionFactory;
        private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
        private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
        private String prefix = "";
    
        public MyRedisTokenStore(RedisConnectionFactory connectionFactory) {
            this.connectionFactory = connectionFactory;
        }
    
        public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
            this.authenticationKeyGenerator = authenticationKeyGenerator;
        }
    
        public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) {
            this.serializationStrategy = serializationStrategy;
        }
    
        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }
    
        private RedisConnection getConnection() {
            return this.connectionFactory.getConnection();
        }
    
        private byte[] serialize(Object object) {
            return this.serializationStrategy.serialize(object);
        }
    
        private byte[] serializeKey(String object) {
            return this.serialize(this.prefix + object);
        }
    
        private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
            return (OAuth2AccessToken) this.serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
        }
    
        private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
            return (OAuth2Authentication) this.serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
        }
    
        private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
            return (OAuth2RefreshToken) this.serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
        }
    
        private byte[] serialize(String string) {
            return this.serializationStrategy.serialize(string);
        }
    
        private String deserializeString(byte[] bytes) {
            return this.serializationStrategy.deserializeString(bytes);
        }
    
        @Override
        public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
            String key = this.authenticationKeyGenerator.extractKey(authentication);
            byte[] serializedKey = this.serializeKey(AUTH_TO_ACCESS + key);
            byte[] bytes = null;
            RedisConnection conn = this.getConnection();
            try {
                bytes = conn.get(serializedKey);
            } finally {
                conn.close();
            }
            OAuth2AccessToken accessToken = this.deserializeAccessToken(bytes);
            if (accessToken != null) {
                OAuth2Authentication storedAuthentication = this.readAuthentication(accessToken.getValue());
                if (storedAuthentication == null || !key.equals(this.authenticationKeyGenerator.extractKey(storedAuthentication))) {
                    this.storeAccessToken(accessToken, authentication);
                }
            }
            return accessToken;
        }
    
        @Override
        public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
            return this.readAuthentication(token.getValue());
        }
    
        @Override
        public OAuth2Authentication readAuthentication(String token) {
            byte[] bytes = null;
            RedisConnection conn = this.getConnection();
            try {
                bytes = conn.get(this.serializeKey("auth:" + token));
            } finally {
                conn.close();
            }
            OAuth2Authentication auth = this.deserializeAuthentication(bytes);
            return auth;
        }
    
        @Override
        public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
            return this.readAuthenticationForRefreshToken(token.getValue());
        }
    
        public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
            RedisConnection conn = getConnection();
            try {
                byte[] bytes = conn.get(serializeKey(REFRESH_AUTH + token));
                OAuth2Authentication auth = deserializeAuthentication(bytes);
                return auth;
            } finally {
                conn.close();
            }
        }
    
        @Override
        public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
            byte[] serializedAccessToken = serialize(token);
            byte[] serializedAuth = serialize(authentication);
            byte[] accessKey = serializeKey(ACCESS + token.getValue());
            byte[] authKey = serializeKey(AUTH + token.getValue());
            byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));
            byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
            byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
    
            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.stringCommands().set(accessKey, serializedAccessToken);
                conn.stringCommands().set(authKey, serializedAuth);
                conn.stringCommands().set(authToAccessKey, serializedAccessToken);
                if (!authentication.isClientOnly()) {
                    conn.rPush(approvalKey, serializedAccessToken);
                }
                conn.rPush(clientId, serializedAccessToken);
                if (token.getExpiration() != null) {
                    int seconds = token.getExpiresIn();
                    conn.expire(accessKey, seconds);
                    conn.expire(authKey, seconds);
                    conn.expire(authToAccessKey, seconds);
                    conn.expire(clientId, seconds);
                    conn.expire(approvalKey, seconds);
                }
                OAuth2RefreshToken refreshToken = token.getRefreshToken();
                if (refreshToken != null && refreshToken.getValue() != null) {
                    byte[] refresh = serialize(token.getRefreshToken().getValue());
                    byte[] auth = serialize(token.getValue());
                    byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue());
                    conn.stringCommands().set(refreshToAccessKey, auth);
                    byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue());
                    conn.stringCommands().set(accessToRefreshKey, refresh);
                    if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                        ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
                        Date expiration = expiringRefreshToken.getExpiration();
                        if (expiration != null) {
                            int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
                                    .intValue();
                            conn.expire(refreshToAccessKey, seconds);
                            conn.expire(accessToRefreshKey, seconds);
                        }
                    }
                }
                conn.closePipeline();
            } finally {
                conn.close();
            }
        }
    
        private static String getApprovalKey(OAuth2Authentication authentication) {
            String userName = authentication.getUserAuthentication() == null ? "" : authentication.getUserAuthentication().getName();
            return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
        }
    
        private static String getApprovalKey(String clientId, String userName) {
            return clientId + (userName == null ? "" : ":" + userName);
        }
    
        @Override
        public void removeAccessToken(OAuth2AccessToken accessToken) {
            this.removeAccessToken(accessToken.getValue());
        }
    
        @Override
        public OAuth2AccessToken readAccessToken(String tokenValue) {
            byte[] key = serializeKey(ACCESS + tokenValue);
            byte[] bytes = null;
            RedisConnection conn = getConnection();
            try {
                bytes = conn.get(key);
            } finally {
                conn.close();
            }
            OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
            return accessToken;
        }
    
        public void removeAccessToken(String tokenValue) {
            byte[] accessKey = serializeKey(ACCESS + tokenValue);
            byte[] authKey = serializeKey(AUTH + tokenValue);
            byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.get(accessKey);
                conn.get(authKey);
                conn.del(accessKey);
                conn.del(accessToRefreshKey);
                // Don't remove the refresh token - it's up to the caller to do that
                conn.del(authKey);
                List<Object> results = conn.closePipeline();
                byte[] access = (byte[]) results.get(0);
                byte[] auth = (byte[]) results.get(1);
    
                OAuth2Authentication authentication = deserializeAuthentication(auth);
                if (authentication != null) {
                    String key = authenticationKeyGenerator.extractKey(authentication);
                    byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
                    byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
                    byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
                    conn.openPipeline();
                    conn.del(authToAccessKey);
                    conn.lRem(unameKey, 1, access);
                    conn.lRem(clientId, 1, access);
                    conn.del(serialize(ACCESS + key));
                    conn.closePipeline();
                }
            } finally {
                conn.close();
            }
        }
    
        @Override
        public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
            byte[] refreshKey = serializeKey(REFRESH + refreshToken.getValue());
            byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + refreshToken.getValue());
            byte[] serializedRefreshToken = serialize(refreshToken);
            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.stringCommands().set(refreshKey, serializedRefreshToken);
                conn.stringCommands().set(refreshAuthKey, serialize(authentication));
                if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                    ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
                    Date expiration = expiringRefreshToken.getExpiration();
                    if (expiration != null) {
                        int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
                                .intValue();
                        conn.expire(refreshKey, seconds);
                        conn.expire(refreshAuthKey, seconds);
                    }
                }
                conn.closePipeline();
            } finally {
                conn.close();
            }
        }
    
        @Override
        public OAuth2RefreshToken readRefreshToken(String tokenValue) {
            byte[] key = serializeKey(REFRESH + tokenValue);
            byte[] bytes = null;
            RedisConnection conn = getConnection();
            try {
                bytes = conn.get(key);
            } finally {
                conn.close();
            }
            OAuth2RefreshToken refreshToken = deserializeRefreshToken(bytes);
            return refreshToken;
        }
    
        @Override
        public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
            this.removeRefreshToken(refreshToken.getValue());
        }
    
        public void removeRefreshToken(String tokenValue) {
            byte[] refreshKey = serializeKey(REFRESH + tokenValue);
            byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + tokenValue);
            byte[] refresh2AccessKey = serializeKey(REFRESH_TO_ACCESS + tokenValue);
            byte[] access2RefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.del(refreshKey);
                conn.del(refreshAuthKey);
                conn.del(refresh2AccessKey);
                conn.del(access2RefreshKey);
                conn.closePipeline();
            } finally {
                conn.close();
            }
        }
    
        @Override
        public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
            this.removeAccessTokenUsingRefreshToken(refreshToken.getValue());
        }
    
        private void removeAccessTokenUsingRefreshToken(String refreshToken) {
            byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken);
            List<Object> results = null;
            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.get(key);
                conn.del(key);
                results = conn.closePipeline();
            } finally {
                conn.close();
            }
            if (results == null) {
                return;
            }
            byte[] bytes = (byte[]) results.get(0);
            String accessToken = deserializeString(bytes);
            if (accessToken != null) {
                removeAccessToken(accessToken);
            }
        }
    
        public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
            byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName));
            List<byte[]> byteList = null;
            RedisConnection conn = getConnection();
            try {
                byteList = conn.lRange(approvalKey, 0, -1);
            } finally {
                conn.close();
            }
            if (byteList == null || byteList.size() == 0) {
                return Collections.<OAuth2AccessToken>emptySet();
            }
            List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
            for (byte[] bytes : byteList) {
                OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
                accessTokens.add(accessToken);
            }
            return Collections.<OAuth2AccessToken>unmodifiableCollection(accessTokens);
        }
    
        @Override
        public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
            byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId);
            List<byte[]> byteList = null;
            RedisConnection conn = getConnection();
            try {
                byteList = conn.lRange(key, 0, -1);
            } finally {
                conn.close();
            }
            if (byteList == null || byteList.size() == 0) {
                return Collections.<OAuth2AccessToken>emptySet();
            }
            List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
            for (byte[] bytes : byteList) {
                OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
                accessTokens.add(accessToken);
            }
            return Collections.<OAuth2AccessToken>unmodifiableCollection(accessTokens);
        }
    }
    
    

      设定获取user的接口,用来作为认证接口(user权限的接口)

    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        Logger logger = LoggerFactory.getLogger(UserController.class);
    
        @RequestMapping(value = "/current", method = RequestMethod.GET)
        public Principal getUser(Principal principal) {
            logger.info(">>>>>>>>>>>>>>>>>>>>>>>>");
            logger.info(principal.toString());
            logger.info(">>>>>>>>>>>>>>>>>>>>>>>>");
            return principal;
        }
    }
    

      为了打开user的端口,还需要资源配置

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig  extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(HttpSecurity http) throws Exception {
     
            http
                    .csrf().disable()
                    .exceptionHandling()
                    .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                    .and()
                    .authorizeRequests()
                    .antMatchers("/actuator/**", "/oauth/**","/token/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin().permitAll();
        }
    }
    
    
    3.2.2 client端实现

      资源接口配置

    @Configuration
    @EnableResourceServer
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class ResourceConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        private MyAccessDecisionManager myAccessDecisionManager;
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
    
            http
                    .csrf().disable()
                    .exceptionHandling()
                    .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                    .and()
    //                .requestMatchers()
    //                .antMatchers("/hello","/hello/**")
    //                .and()
                    .authorizeRequests()
                    .antMatchers("/hello","/hello/**").authenticated().accessDecisionManager(myAccessDecisionManager)
                    /**
                    antMatchers("/hello","/hello/**").authenticated().accessDecisionManager(myAccessDecisionManager)
                    是用来确认是不是能用AccessDecisionMangager是不是能搞定权限(答案是可以的),不需要的可以用上面的方法就好了
                    */
    //                .and()
    //                .authorizeRequests()
    //                .antMatchers("/hello","/hello/**").authenticated()
    //                .and()
    //                .authorizeRequests()
    //                .antMatchers("/hello","/hello/**").hasAuthority(null)
                    .and()
                    .authorizeRequests()
                    .antMatchers("/actuator/**", "/oauth/**","/token/**").permitAll()
                    .and()
                    .formLogin().permitAll();
        }
    }
    

      MyDecideAccessDecisionManager的实现,主要是decide方法;完全是为了测试,所以里面的方法都是自己瞎定义的,完全为了方便

    @Slf4j
    @Service
    public class MyAccessDecisionManager implements AccessDecisionManager {
        public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributeCollection)
                throws AccessDeniedException, InsufficientAuthenticationException{
            HttpServletRequest httpRequest = ((FilterInvocation)object).getHttpRequest();
            StringBuffer stringBuffer=new StringBuffer();
            authentication.getAuthorities().stream().forEach(s->stringBuffer.append(s));
            log.info("Authentication information:"+stringBuffer);
            log.info("url information:"+httpRequest.getRequestURI());
            StringBuffer stringBufferConfigAttributeCollection=new StringBuffer();
            authentication.getAuthorities().stream().forEach(s->stringBufferConfigAttributeCollection.append(s));
            log.info("configAttributeCollection information:"+stringBufferConfigAttributeCollection);
            if(httpRequest.getRequestURI().equals("/hello/test")){
                return;
            }
            /**
             * 测试证明PreAuthorize还是会参与权限判断,但是可以通过decide去做了
             * 不需要使用PreAuthorize再去做一次
             */
            if(httpRequest.getRequestURI().equals("/hello")){
                return;
            }
            throw new AccessDeniedException("AccessDecisionManager Access Denied");
        }
    
        public boolean supports(ConfigAttribute var1){
            return true;
        }
    
        public boolean supports(Class<?> var1){
            return true;
        }
    }
    
    

      测试使用的controller

    @RestController
    public class TestController {
        @GetMapping("/hello")
        public String hello(){
            return "hello";
        }
    
        @PreAuthorize(value=("hasAuthority('Role_Admin')"))
        @GetMapping("/oauth2")
        public String oauth(){
            return "hello,oauth2";
        }
    
        @PreAuthorize(value=("hasAuthority('Role_Admin')"))
        @GetMapping("/hello/test")
        public String test(){
            return "hello,test";
        }
    }
    

      client端的配置文件

    server:
      port: 8091
    
    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/tms?useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&useSSL=false
        username: root
        password: 
      application:
        name: test-service
    
    mybatis:
      mapper-locations: classpath:mapping/*.xml
      type-aliases-package: com.dfcj.oauth2security.dao
    
    #logging:
    #  level:
    #    root: debug
    
    ###用来认证客户端使用的,很重要
    security:
      oauth2:
        resource:
          user-info-uri: http://localhost:8090/users/current
          id: test-service
          prefer-token-info: false
        client:
          scope: service
          client-secret: 123456
          client-id: user-service
          id: user-service
    

    4.总结

      写完通篇,我发现我自己只是贴了一些自认为的重要代码,解释啥的也没说啥(其实我也不知道应该说啥,只是知道这个不简单)
      自己一开始从最简单的client模式实现,然后转到password模式,然后又用password模式实现授权,然后又转到实现AccessDecisionManager去实现鉴权,辗转反复,一直在测试一直在debug,终于弄出来一点小的成果,和大家分享一下,可能看不懂,但是希望对后面做oauth2的人有帮助吧。

    有问题欢迎提问,一起解决java问题

    展开全文
  • OAuth2.0 原理流程及其单点登录和权限控制

    万次阅读 多人点赞 2018-07-27 11:51:00
    本文以现实生活场景辅助理解,力争彻底理清 OAuth2.0 实现单点登录的原理流程。同时总结了权限控制的实现方案,及其在微服务架构中的应用。 作者:王克锋 出处:https://kefeng.wang/2018/04/06/oauth2-sso/ ...

    单点登录是多域名企业站点流行的登录方式。本文以现实生活场景辅助理解,力争彻底理清 OAuth2.0 实现单点登录的原理流程。同时总结了权限控制的实现方案,及其在微服务架构中的应用。

    作者:王克锋
    出处:https://kefeng.wang/2018/04/06/oauth2-sso/
    版权:自由转载-非商用-非衍生-保持署名,转载请标明作者和出处。

    1 什么是单点登录

    1.1 多点登录

    传统的多点登录系统中,每个站点都实现了本站专用的帐号数据库和登录模块。各站点的登录状态相互不认可,各站点需要逐一手工登录。如下图,有两个术语含义如下:

    • 认证(authentication): 验证用户的身份;
    • 授权(authorization): 验证用户的访问权限。

    1.2 单点登录

    单点登录,英文是 Single Sign On,缩写为 SSO。
    多个站点(192.168.1.20X)共用一台认证授权服务器(192.168.1.110,用户数据库和认证授权模块共用)。用户经由其中任何一个站点(比如 192.168.1.201)登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。

    2 OAuth2 认证授权的原理流程

    2.1 生活实例【★★重点★★】

    为了直观的理解 OAuth2.0 原理流程,我们假设这样一个生活场景:
    (1)档案局A(客户端 / Client):以“档案局ID/密码”标识,是掌握档案资源的机构。并列还有很多档案局B/C/…,每个档案局存储的档案内容(资源 / Resource)不一样,比如政治、经济、军事、文化等;
    (2)公民张三(资源所有者 / Resource Owner):以“用户名/密码”标识,需要到各个档案局查档案;
    (3)派出所(授权服务器 / Authentication Server):可以是单个巨大的派出所,也可以是数据共享的派出所集群,掌管的信息、提供的对外接口功能有:

    • 档案局信息:所有档案局的“档案局ID/密码”,证明档案局的身份;
    • 公民信息:所有公民的“用户名/密码”,能提供张三是张三的用户身份证明(认证 / Authentication)
    • 公民对于档案局的权限:有张公民和档案局的权限的映射表,可查得各公民对各档案局是否有操作权限(授权 / Authorization)。通常,设计中会增加官职(角色 / Role)一层,各公民属于哪个官职(角色),哪个官职(角色)对于特定档案局有操作权限。

    2.1.1 张三首次访问档案局A

    张三之前从未到访档案局,第一次来档案局。对照下图序号理解:
    (1)张三来到“档案局A”的“档案处”,该处要求实名登记后才能查询,被指示到“用户登记处”办理(HTTP重定向);
    (2)张三来到“档案局A”的“用户登记处”,既不能证明身份(认证),又不能证明自己有查档案A的权限(授权)。张三携带档案局A的标识(client-id),被重定向至“授权信开具处”;
    (3)张三来到“派出所”的“授权信开具处”,出示档案局A的标识,希望开具授权信(授权)。该处要求首先证明身份(认证),被重定向至“用户身份验证处”;
    (4)张三来到“派出所”的“用户身份验证处”,领取了用户身份表(网页登录表单 Form);
    (5)张三填上自己的用户名和密码,交给(提交 / Submit)“用户身份验证处”,该处从私用数据库中查得用户名密码匹配,确定此人是张三,开具身份证明信,完成认证。张三带上身份证明信和档案局A的标识,被重定向至“授权信开具处”;
    (6)张三再次来到“授权信开具处”,出示身份证明信和档案局A的标识,该处从私用数据库中查得,张三的官职是市长级别(角色),该官职具有档案局A的查询权限,就开具“允许张三查询档案局A”的授权信(授权码 / code),张三带上授权信被重定向至“档案局”的“用户登录处”;
    (7)张三到了“档案局”的“用户登录处”,该处私下拿出档案局A的标识(client-id)和密码,再附上张三出示的授权信(code),向“派出所”的“腰牌发放处”为张三申请的“腰牌”(token),将来张三可以带着这个腰牌表明身份和权限。又被重定向到“档案处”;
    (8)张三的会话(Session)已经关联上了腰牌(token),可以直接通过“档案处”查档案。

    2.1.2 张三首次访问档案局B

    张三已经成功访问了档案局A,现在他要访问档案局B。对照下图序号理解:
    (1)/(2) 同上;
    (3)张三已经有“身份证明信”,直接在“派出所”的“授权信开具处”成功开具“访问档案局B”的授权信;
    (4)/(5)/(6) 免了;
    (7)“档案局B”的“用户登记处”完成登记;
    (8)“档案局B”的“档案处”查得档案。

    2.1.3 张三再次访问档案局A

    张三已经成功访问了档案局A,现在他要访问档案局A。对照下图序号理解:
    (1)直接成功查到了档案;
    (2~8)都免了。

    2.2 HTTP 重定向原理

    HTTP 协议中,浏览器的 REQUEST 发给服务器之后,服务器如果发现该业务不属于自己管辖,会把你支派到自身服务器或其他服务器(host)的某个接口(uri)。正如我们去政府部门办事,每到一个窗口,工作人员会说“你带上材料A,到本所的X窗口,或者其他Y所的Z窗口”进行下一个手续。

    2.3 SSO 工作流程

    至此,就不难理解 OAuth 2.0 的认证/授权流程,此处不再赘述。请拿下图对照“2.1 生活实例”一节来理解。

    2.4 OAuth2.0 进阶

    根据官方标准,OAuth 2.0 共用四种授权模式:

    • Authorization Code: 用在服务端应用之间,这种最复杂,也是本文采用的模式;
    • Implicit: 用在移动app或者web app(这些app是在用户的设备上的,如在手机上调起微信来进行认证授权)
    • Resource Owner Password Credentials(password): 应用直接都是受信任的(都是由一家公司开发的,本例子使用)
    • Client Credentials: 用在应用API访问。

    3 基于 SpringBoot 实现认证/授权

    官方文档:Spring Cloud Security

    3.1 授权服务器(Authorization Server)

    (1) pom.xml

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>

    (2) application.properties

    server.port=8110 ## 监听端口

    (3) AuthorizationServerApplication.java

    @EnableResourceServer // 启用资源服务器
    public class AuthorizationServerApplication {
        // ...
    }

    (4) 配置授权服务的参数

    @Configuration
    @EnableAuthorizationServer
    public class Oauth2AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
        @Override
        public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("webapp").secret("secret") //客户端 id/secret
                    .authorizedGrantTypes("authorization code") //授权妈模式
                    .scopes("user_info")
                    .autoApprove(true) //自动审批
                    .accessTokenValiditySeconds(3600); //有效期1hour
        }
    }
    
    @Configuration
    public class Oauth2WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.requestMatchers()
                    .antMatchers("/login", "/oauth/authorize/oauth/logout")
                    .and().authorizeRequests().anyRequest().authenticated()
                    .and().formLogin().permitAll();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication().withUser("admin").password("admin123").roles("ADMIN");
        }
    }

    3.2 客户端(Client, 业务网站)

    (1) pom.xml

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>

    (2) application.properties

    server port=8080
    security.oauth2.client.client-id=webapp
    security.oauth2.client.client-secret=secret
    security.oauth2.client.access-token-uri=http://localhost:8110/oauth/token
    security.oauth2.client.user-authorization-uri=http://localhost:8110/oauth/authorize
    security.oauth2.resource.user-info-uri=http://localhost:8110/oauth/user

    (3) 配置 WEB 安全

    @Configuration
    @EnableOAuth2Sso
    public class Oauth2WebsecurityConfigurer extends WebSecurityConfigurerAdapter {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/**").authorizeRequests()
                    .antMatchers("/", "/login").permitAll()
                    .anyRequest().authenticated();
        }
    }
    
    @RestController
    public class Oauth2ClientController {
        @GetMapping("/")
        public ModelAndView index() {
            return new ModelAndView("index");
        }
    
        @GetMapping("/welcome")
        public ModelAndView welcome() {
            return new ModelAndView("welcome");
        }
    }

    3.3 用户权限控制(基于角色)

    • 授权服务器中,定义各用户拥有的角色: user=USER, admin=ADMIN/USER, root=ROOT/ADMIN/USER
    • 业务网站中(client),注解标明哪些角色可
    @RestController
    public class Oauth2ClientController {
        @GetMapping("/welcome")
        public ModelAndView welcome() {
            return new ModelAndView("welcome");
        }
    
        @GetMapping("/api/user")
        @PreAuthorize("hasAuthority('USER')")
        public Map<String, Object> apiUser() {
        }
    
        @GetMapping("/api/admin")
        @PreAuthorize("hasAuthority('ADMIN')")
        public Map<String, Object> apiAdmin() {
        }
    
        @GetMapping("/api/root")
        @PreAuthorize("hasAuthority('ROOT')")
        public Map<String, Object> apiRoot() {
        }
    }

    4 综合运用

    4.1 权限控制方案

    下图是基本的认证/授权控制方案,主要设计了认证授权服务器上相关数据表的基本定义。可对照本文“2.1 生活实例”一节来理解。

    4.2 在微服务架构中的应用

    与常规服务架构不同,在微服务架构中,Authorization Server/Resource Server 是作为微服务存在的,用户的登录可以通过API网关一次性完成,无需与无法跳转至内网的 Authorization Server 来完成。

    展开全文
  • 适用人群 ...SpringBoot+OAuth2+Jwt+Swagger等,核心技术采用Nacos、Fegin、Ribbon、Gateway、Hystrix、JWT Token、Mybatis、SpringBoot、Redis等主要框架和中间件。SpringCloud整合详细如下:
  • OAuth 简介

    2019-05-19 12:22:47
    常用的应用 OAuth 的场景,一般是某个网站想要获取一个用户在第三方网站中的某些资源和服务。比如在人人网上,想要导入用户MSN里的好友,在没有OAuth时,可能需要用户向人人网提供MSN用户名和密码。这种做法使得人人...

    OAuth是一个在不提供用户名和密码的情况下,授权第三方应用访问Web资源的安全协议。

    常用的应用 OAuth 的场景,一般是某个网站想要获取一个用户在第三方网站中的某些资源和服务。比如在人人网上,想要导入用户MSN里的好友,在没有OAuth时,可能需要用户向人人网提供MSN用户名和密码。这种做法使得人人网会持有用户的MSN账户和密码,虽然人人网承诺持有密码后的安全,但这其实扩大了攻击面,用户也难以无条件地信任人人网。

    而OAuth 则解决了这个信任的问题,它使得用户在不需要向人人网提供MSN用户名和密码的情况下,可以授权MSN将用户的好友名单提供给人人网。

    在OAuth 1.0中,涉及三个角色,分别是:

    • Consumer:消费方(Client)
    • Service Provider:服务提供方(Server)
    • User:用户(Resource Owner)

    在新版本的OAuth中,又被称为Client、Server、Resource Owner。在上面的例子中,Client是人人网,Server是MSN,Resource Owner是用户。

    我们再来看一个实际的场景。假设 Jane 在 faji.com上有两张照片,他想将这两张照片分享到 beppa.com,通过OAuth,这个过程是如何实现的呢?

    Jane 在 beppa.com 上,选择要从 faji.com 上分享照片。

    在 beppa.com 后台,则会创建一个临时凭证(Temporary Credentials), 稍后 Jane 将持此临时凭证前往 faji.com。

     

    然后页面跳转到 faji.com 的 OAuth 页面,并要求 Jane 登录。注意这是在faji.com 上登录!

    登录成功后,faji.com 会询问 Jane 是否授权 beppa.com 访问 Jane 在faji.com 里的私有照片。

    如果Jane 授权成功,faji.com 会将Jane带来的临时凭证(Temporary Credentials)标记为 ”Jane 已授权“,同时跳转回 beppa.com, 并带上临时凭证(Temporary Credentials)。凭此,beppa.com 知道他可以去获取Jane的私有照片了。

    对于 beppa.com 来说,它首先通过Request Token 去 faji.com 换取 Access Token,然后就可以用Access Token 访问资源了。Request Token 只能用于获取用户的授权,Access Token才能用于访问用户的资源。

    最终,Jane 成功地将他的照片从 faji.com 分享到 beppa.com上。

    我们也可以参考如下新浪微博开放平台的OAuth 的授权过程,它与上面描述的过程是一样的。

    常用的需要用到OAuth 的地方有桌面应用、手机设备、Web应用,但OAuth 1.0 只提供了统一的接口。这个接口对于Web应用来说尚可使用,但手机设备和桌面应用用起来则会有些别扭。同时OAuth 1.0 的应用架构在扩展性方面也存在一些问题,当用户请求数庞大时,可能会遇到一些性能瓶颈。为了改变这些问题,OAuth 2.0 应用而生。

    展开全文
  • OAuth授权 看这一篇就够了

    千次阅读 2019-06-16 23:07:28
    今天我们来谈一谈近几年来非常流行的,大名鼎鼎的OAuth。它也能完成统一认证,而且还能做更多的事情。至于OAuth与SSO的区别,将在文章最后总结。 如上图所示,用户通过浏览器(Browser)访问app1,他想用微信的...

    背景

    上一篇我们介绍了单点登录(SSO),它能够实现多个系统的统一认证。今天我们来谈一谈近几年来非常流行的,大名鼎鼎的OAuth。它也能完成统一认证,而且还能做更多的事情。至于OAuth与SSO的区别,将在文章最后总结。

    image1

    如上图所示,用户通过浏览器(Browser)访问app1,他想用微信的账号直接登录,这样就免去了在app1系统的注册流程。这样的流程完全符合单点登录(SSO),但我们今天要看看OAuth是怎么做的。

    具体流程

    image2

    流程比单点登录(SSO)复杂了很多,但是它比SSO更强大。接下来我们好好捋一捋这个流程:

    1. 用户访问app1系统,app1返回登录页,让用户登录。
    2. 用户点击微信登录,这里的微信就是OAuth Server,跳转到微信登录页,带上参数appid和回调地址(backUrl)。关于appid我们要详细
      说一下,我们在与OAuth Server做对接的时候,先要在OAuth Server上注册自己的系统(app1),需要填写应用的名称、回调地址等, OAuth Server会生成appid和appSecret,这两个变量是非常关键的。
    3. 微信后台(OAuth Server)根据appid查找到app1的注册信息,校验参数backUrl和注册的回调地址是否一致(这里可以只校验域名或者一级域名,具体要看OAuth Server怎么设计),如果校验不通过则返回错误,校验成功则返回授权页。
    4. 微信弹出授权页,如果微信没有登录则弹出登录并授权页。这个过程是微信询问用户,是否同意app1系统访问微信的资源。用户授权后, 微信后台(OAuth Server)会生成这个用户对应的code,并通过app1的backUrl返回app1系统。
    5. app1系统拿到code后,再带上appid和appSecret从后台访问微信后台(OAuth Server),换取用户的token。
    6. app1拿到token后,再去微信后台(OAuth Server)获取用户的信息。
    7. app1设置session为登录状态,并将用户信息(昵称、头像等)返回给Browser。

    到这里,OAuth的授权流程就结束了。有的同学可能很快就会问到:用户授权后,为什么不直接返回token,而是要用code换取token?这是因为如果直接返回token,token会先到浏览器(Browser),然后再到app1系统,到了浏览器,这个token就是不安全的了,有可能被窃取,token被窃取后,微信后台(OAuth Server)中,这个用户的信息就不安全了。然而在用code换取token时,是带上了appid和appSecret的,微信后台(OAuth Server)可以判断appid和appSecret的合法性,确认无误后,再将token返回给app1。这里是直接返回app1,没有经过浏览器,token是安全的。

    静默登录

    上面的流程是用户先访问app1,点击微信登录后,跳到微信。如果我们先打开了微信,在微信里边再打开app1,这个流程就好像我们在
    微信里打开了京东,这里微信就是OAuth Server,京东就是app1。在这个流程中,我们可以省略掉询问用户是否授权的过程,也就是在微信里打开京东(app1)的时候,京东(app1)带着appid和backUrl访问微信(OAuth Server),微信(OAuth Server)验证appid和backUrl,并且微信(OAuth Server)已经是登录的,这里并没有询问用户是否授权京东访问自己在微信的资源,直接将code返回给了京东。从而使京东登录,并且在京东里显示的用户在微信中的昵称和头像等信息。

    使用静默登录一般都是你的系统做的比较牛了,被资源方看中了,要把你的系统嵌入到资源方去。比如:微信中嵌入了京东。

    上面的例子中,我们只做了获取用户信息,其实还可以开放很多信息,例如:用户的账户余额等。要开放哪些资源就看OAuth Server的了。
    这也就是我们常说的open api。要做open api,上面的OAuth流程是必不可少的。

    与单点登录(SSO)的对比

    单点登录(SSO)是保障客户端(app1)的用户资源的安全 。

    OAuth则是保障服务端(OAuth)的用户资源的安全 。


    单点登录(SSO)的客户端(app1)要获取的最终信息是,这个用户到底有没有权限访问我(app1)的资源。

    OAuth获取的最终信息是,我(OAuth Server)的用户的资源到底能不能让你(app1)访问。


    单点登录(SSO),资源都在客户端(app1)这边,不在SSO Server那一方。 用户在给SSO Server提供了用户名密码后,作为客户端app1并不知道这件事。 随便给客户端(app1)个ST,那么客户端(app1)是不能确定这个ST是否有效,所以要拿着这个ST去SSO Server再问一下,这个ST是否有效,是有效的我(app1)才能让这个用户访问。

    OAuth认证,资源都在OAuth Server那一方,客户端(app1)想获取OAuth Server的用户资源。 所以在最安全的模式下,用户授权之后,
    服务端(OAuth Server)并不能通过重定向将token发给客户端(app1),因为这个token有可能被黑客截获,如果黑客截获了这个token,那么用户的资源(OAuth Server)也就暴露在这个黑客之下了。 于是OAuth Server通过重定向发送了一个认证code给客户端(app1),客户端(app1)在后台,通过https的方式,用这个code,以及客户端和服务端预先商量好的密码(appid和appSecret),才能获取到token,这个过程是非常安全的。 如果黑客截获了code,他没有那串预先商量好的密码(appid和appSecret),他也是无法获取token的。这样OAuth就能保证请求资源这件事,是用户同意的,客户端(app1)也是被认可的,可以放心的把资源发给这个客户端(app1)了。

    总结:所以单点登录(SSO)和OAuth在流程上的最大区别就是,通过ST或者code去认证的时候,需不需要预先商量好的密码(appid和appSecret)。

    总结

    OAuth和SSO都可以做统一认证登录,但是OAuth的流程比SSO复杂。SSO只能做用户的认证登录,OAuth不仅能做用户的认证登录,开可以做open api开放更多的用户资源

     

    链文

    展开全文
  • OAuth 2 深入介绍

    2019-04-09 23:41:15
    OAuth 2 通过将用户身份验证委派给托管用户帐户的服务以及授权客户端访问用户帐户进行工作。综上,OAuth 2 可以为 Web 应用 和桌面应用以及移动应用提供授权流程。 本文将从OAuth 2 角色,授权许...
  • 重点:以下内容来自于阮一峰老师写的资料: http://www.ruanyifeng.com/blog/2019/04/oauth_design.html ...OAuth 2.0是目前最流行的授权机制,用来授权第三方应用,获取用户数据。 这个标准比较抽象,使用了很...
  • OAuth2.0认证原理浅析

    万次阅读 多人点赞 2019-01-21 18:16:21
    一.OAuth是什么?  OAuth的英文全称是Open Authorization,它是一种开放授权协议。OAuth目前共有2个版本,2007年12月的1.0版(之后有一个修正版1.0a)和2010年4月的2.0版,1.0版本存在严重安全漏洞,而2.0版解决了...
  • OAuth 2.0 认证的原理与实践

    万次阅读 2017-03-24 01:44:31
    使用 OAuth 2.0 认证的的好处是显然易见的。你只需要用同一个账号密码,就能在各个网站进行访问,而免去了在每个网站都进行注册的繁琐过程。 本文将介绍 OAuth 2.0 的原理,并基于 Spring Security 和 GitHub 账号,...
  • OAuth原理,图文并茂,通俗易懂

    千次阅读 2018-03-07 00:06:01
    步骤2请求OAuth登录页(慕课的服务器请求腾讯服务器)步骤3使用第三方账号登录并授权步骤4返回登录结果拿到了加密后的code参数,有了code基本上可以确定(用户输入的QQ号和密码)是匹配的,也就是说登录是成功的。...
  • OAuth2学习(一)——初识OAuth2

    万次阅读 多人点赞 2019-08-28 19:17:08
    今天我们来讲解一下OAuth2,在平时应用中我们经常能够见到它的身影。比如,当微信小程序获取你的用户名和头像时需要你授予权限,以及当我们在网站上使用微信或QQ登录时也是使用到了OAuth2。接下来我们便来讲解一下...
  • Spring Security 与 Oauth2 整合 步骤

    万次阅读 多人点赞 2015-07-26 22:18:58
    1. spring-security-oauth2的demo 不容易让开发者理解, 配置的内容很多, 没有分解的步骤; 我曾经试着按照文档(https://github.com/spring-projects/spring-security-oauth/blob/master/docs/oauth2.md) 配置了几次, ...
  • Re:从零开始的Spring Security Oauth2(一)

    万次阅读 多人点赞 2017-08-11 13:51:34
    关于oauth2,其实是一个规范,本文重点讲解spring对他进行的实现,如果你还不清楚授权服务器,资源服务器,认证授权等基础概念,可以移步理解OAuth 2.0 - 阮一峰,这是一篇对于oauth2很好的科普文章。 需要对spring
  • 1.什么是OAuth2.0OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。 OAuth(开放授权)是一个开放标准。允许第三方网站在用户授权的前提下访问在用户在服务商那里存储...
  • 基于Spring Security的Oauth2授权实现

    万次阅读 2018-08-27 12:17:13
    经过一段时间的学习Oauth2,在网上也借鉴学习了一些大牛的经验,推荐在学习的过程中多看几遍阮一峰的《理解OAuth 2.0》,经过对Oauth2的多种方式的实现,个人推荐Spring Security和Oauth2的实现是相对优雅的,理由...
  • Java的oauth2.0 服务端与客户端的实现

    万次阅读 多人点赞 2017-10-02 12:33:28
    oauth原理简述 oauth本身不是技术,而是一项资源授权协议,重点是协议!Apache基金会提供了针对Java的oauth封装。我们做Java web项目想要实现oauth协议进行资源授权访问,直接使用该封装就可以。 想深入研究原理的 ...
  • 谈谈OAuth1,OAuth2异同

    千次阅读 2018-08-09 08:59:06
    在收集资料时,我查询和学习了许多介绍OAuth的文章,这些文章有好有坏,但大多是从个例出发。因此我想从官方文档出发,结合在stackoverflow上的一些讨论,一并整理一下。整理的内容分为OAuth1.0a和OAuth2两部分。 ...
  • 一、OAuth2.0介绍 GitHub地址案例代码地址 1.概念说明   先说OAuthOAuth是Open Authorization的简写。   OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAuth的...
  • OAuth2 in Action

    2020-07-30 23:32:08
    You'll learn how to confidently and securely build and deploy OAuth on both the client and server sides. Foreword by Ian Glazer. Purchase of the print book includes a free eBook in PDF, Kindle, and ...
  • OAuth1,OAuth2异同

    2019-04-04 17:02:52
    在收集资料时,我查询和学习了许多介绍OAuth的文章,这些文章有好有坏,但大多是从个例出发。因此我想从官方文档出发,结合在stackoverflow上的一些讨论,一并整理一下。整理的内容分为OAuth1.0a和OAuth2两部分。 ...
1 2 3 4 5 ... 20
收藏数 64,549
精华内容 25,819
关键字:

oauth