美文网首页Javajava web
06 Spring Security OAuth开发APP认证框

06 Spring Security OAuth开发APP认证框

作者: Koko_滱滱 | 来源:发表于2018-04-23 23:21 被阅读467次

    Spring Security OAuth简介

    浏览器认证成功后,认证信息会存储在服务器的session中,以后再来访问的时候,通过jsessionid去验证session中是否存在认证信息,进而判断是否需要认证。

    image.png
    在现代应用中,后端往往不直接和浏览器交互。
    如果客户端是APP,一般是不会在请求中带jsessionid的,而在前后端分离的架构中,浏览器把请求发送给Web服务器,由Web服务器发送请求到后段服务器中,此时也是不带jsessionid的。

    APP认证与浏览器访问最大的区别,就是没有cookie,jsessionid等
    当然,不是不能用cookie、jsessionid这种认证方式,只要模拟浏览器还是能办到的,但是会带来一些问题。

    1、开发繁琐
    2、安全性和客户体验差
    一旦jsessionid被知道,就很危险。
    3、有些前端技术不支持Cookie(微信小程序)

    image.png

    解决方案:访问服务器的时候每次都带上token(令牌)
    应用服务器不再把用户信息存储再session中,而是根据每次用户请求带来的token,查询他是谁,他能干什么。

    实现标准的OAuth服务提供商

    开启认证服务器支持

    package com.imooc.security.app;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    
    /**
     * @Author 张柳宁
     * @Description
     * @Date Create in 2018/4/23
     * @Modified By:
     */
    @Configuration
    @EnableAuthorizationServer//开启认证服务器支持
    public class ImoocAuthorizationServerConfig {
    }
    

    系统内部已经实现了4种授权模式,使用注解开启后,我们就可以很方便的进行使用了。

    启动服务后,会有几个url服务:

    # 确认授权
    Mapped "{[/oauth/authorize]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
    
    Mapped "{[/oauth/authorize],methods=[POST],params=[user_oauth_approval]}" onto public org.springframework.web.servlet.View org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny(java.util.Map<java.lang.String, java.lang.String>,java.util.Map<java.lang.String, ?>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
    
    # 用授权码获取token
    Mapped "{[/oauth/token],methods=[GET]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
    
    Mapped "{[/oauth/token],methods=[POST]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
    
    Mapped "{[/oauth/check_token]}" onto public java.util.Map<java.lang.String, ?> org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint.checkToken(java.lang.String)
    
    Mapped "{[/oauth/confirm_access]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint.getAccessConfirmation(java.util.Map<java.lang.String, java.lang.Object>,javax.servlet.http.HttpServletRequest) throws java.lang.Exception
    
    Mapped "{[/oauth/error]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelErrorEndpoint.handleError(javax.servlet.http.HttpServletRequest)
    
    

    在日志中还看到了

    security.oauth2.client.client-id = ec70102d-b767-4c97-8b48-0537edd95132
    security.oauth2.client.client-secret = fc6c5a6b-c1eb-45dc-897a-0984b7285f71
    

    这两个参数也是有用的
    为了防止每次都是一个随机生成的值,也可以自己配置

    security:
      basic:
    #    开启 Spring Security
        enabled: true
      oauth2:
        client:
          client-id: imooc
          client-secret: imooc
    

    获取授权码 /oauth/authorize

    参数
    - response_type=code
    - client_id=ec70102d-b767-4c97-8b48-0537edd95132
    - redirect_uri=http://www.baidu.com
    这个参数是可选的,只是为了看到授权码
    - scope=all

    完整的请求:http://localhost:8080/oauth/authorize?response_type=code&client_id=ec70102d-b767-4c97-8b48-0537edd95132&redirect_uri=http://example.com&scope=all
    
    image.png

    在这里,认证服务器扮演了腾讯的角色,client_id扮演了第三方应用的角色。
    而这个用户名,是说client_id要申请腾讯哪个用户的权限。而请求这个用户的什么权限,是通过请求种的scope参数决定的.
    scope是由服务提供商,如:腾讯定义的。
    这个也很好理解。一个用户有哪些权限,需要划分出哪些权限,当然由提供用户服务的服务提供商是最清楚的。

    这里的用户名密码校验由我们的 UserDetailsService 接口实现来定。

    image.png

    这个是授权页面

    image.png

    这个code值就是授权码

    UserDetailService

    package com.imooc.security;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.social.security.SocialUser;
    import org.springframework.social.security.SocialUserDetails;
    import org.springframework.social.security.SocialUserDetailsService;
    import org.springframework.stereotype.Component;
    
    /**
     * 自定义认证逻辑
     */
    @Component
    public class DemoUserDetailsService implements UserDetailsService, SocialUserDetailsService {
    
        private Logger logger = LoggerFactory.getLogger(DemoUserDetailsService.class);
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            logger.info("查找登陆用户名:{}", username);
            //模拟根据用户名查找用户信息
    
            return buildUser(username);
        }
    
        @Override
        public SocialUserDetails loadUserByUserId(String id) throws UsernameNotFoundException {
            logger.info("查找用户ID:{}", id);
            //模拟根据用户名查找用户信息
    
            return buildUser(id);
        }
    
        private SocialUserDetails buildUser(String userId) {
            // 根据用户名查找用户信息
            //根据查找到的用户信息判断用户是否被冻结
            String password = passwordEncoder.encode("123456");
            logger.info("数据库密码是:" + password);
            return new SocialUser(userId, password,
                    true, true, true, true,
                    AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_USER"));
        }
    
    
    }
    

    注意,必须有 ROLE_USER 角色

    获取令牌 /oauth/token

    授权码模式

    这是个post请求

    image.png

    code值就是上一个请求返回的,跳转到url后面带着的code参数值

    url:http://localhost:8080/oauth/token

    返回

    {
        "access_token": "785ffd11-3958-49e9-b60a-464faace3e34",
        "token_type": "bearer",
        "refresh_token": "72b0b0df-b1b2-4847-8be8-0ccda432ff6f",
        "expires_in": 43199,
        "scope": "all"
    }
    

    用户名密码模式

    image.png

    和授权码模式相比,就是请求的参数不同,一个是通过授权码获取token,一个是通过用户名密码获取token

    在用户名密码模式下,实际上用户是把在服务提供商(如腾讯)中的用户名密码告诉了第三方,不是很安全。

    用户名密码模式适用于 服务提供商和第三方调用方是同一家公司产品的情况。

    不管是哪种模式,对于同一个用户,如果token没有过期,那么获取到的token值是不会变的。

    实现资源服务器

    在上一节中,我们实现了一个认证服务器,可通过授权码模式或密码模式,从认证服务器上获取token。

    package com.imooc.security.app;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    
    /**
     * @Author 张柳宁
     * @Description
     * @Date Create in 2018/4/23
     * @Modified By:
     */
    @Configuration
    @EnableResourceServer//开启资源服务器支持
    public class ImoocResourceServerConfig {
    }
    
    

    然后所有的Rest服务在访问前,就需要配置token

    如果直接访问的话,


    image.png

    我们按照前面的,不管是授权码模式还是用户名密码模式获取到token,然后
    http://localhost:8080/user?access_token=e756384d-bf52-4799-bda8-c6933ccbb031
    把token值加在后面作为access_token参数,就可以正常访问了。

    另一种传递token的方式是在头部

    image.png
    我觉得这种方式反而更常见,通用一点。
    第一种传递token的方式第一是会暴露在url上,第二是只能get方式传递。

    目前存在的一些问

    1、token存在内存中
    2、只能用户名密码获取token,不能使用如发送个短信就获取token这种需求
    3、token值如何定制
    ...

    基本Token参数配置

    package com.imooc.security.app;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
    
    /**
     * @Author 张柳宁
     * @Description
     * @Date Create in 2018/4/23
     * @Modified By:
     */
    @Configuration
    @EnableAuthorizationServer//开启认证服务器支持
    public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService);
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()//配置内存中,也可以是数据库
                    .withClient("clientId-imooc")//clientid
                    .secret("clientSecret-imooc")
                    .accessTokenValiditySeconds(3600)//token有效时间  秒
                    .authorizedGrantTypes("refresh_token", "pasword", "authorization_code")//token模式
                    .scopes("all", "read", "write")//限制允许的权限配置
    
                    .and()//下面配置第二个应用   (不知道动态的是怎么配置的,那就不能使用内存模式,应该使用数据库模式来吧)
                    .withClient("test")
                    .scopes("testSc")
                    .accessTokenValiditySeconds(7200)
                    .scopes("all");
        }
    }
    
    

    如何通过配置,动态识别配置的多个客户端呢?
    在yaml中,除了配置基本类型外,也可以是数组

    a:
        b:
            c[0]:
                p1: 111
                p2: 222
            c[1]:
                p1: 233
                p2: 444
    

    c就是数组对象,p1、p2就是数组对象元素的成员变量。
    在configure中读取配置文件后动态配置即可。

    image.png

    注意,如果设置token的过期时间位0,就相当于永远不会过期。
    这个要注意一下

    使用Redis存储token

    package com.imooc.security.app;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
    
    /**
     * @Author 张柳宁
     * @Description
     * @Date Create in 2018/4/23
     * @Modified By:
     */
    @Configuration
    public class TokenStoreConfig {
    
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
    
        @Bean
        public TokenStore redisTokenStore(){
            return new RedisTokenStore(redisConnectionFactory);
        }
    
    }
    
    
    package com.imooc.security.app;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    
    /**
     * @Author 张柳宁
     * @Description
     * @Date Create in 2018/4/23
     * @Modified By:
     */
    @Configuration
    @EnableAuthorizationServer//开启认证服务器支持
    public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Autowired
        private TokenStore redisTokenStore;
    
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(redisTokenStore)//使用Redis作为Token的存储
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService);
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()//配置内存中,也可以是数据库
                    .withClient("clientId-imooc")//clientid
                    .secret("clientSecret-imooc")
                    .accessTokenValiditySeconds(3600)//token有效时间  秒
                    .authorizedGrantTypes("refresh_token", "password", "authorization_code")//token模式
                    .scopes("all", "read", "write")//限制允许的权限配置
    
                    .and()//下面配置第二个应用   (不知道动态的是怎么配置的,那就不能使用内存模式,应该使用数据库模式来吧)
                    .withClient("test")
                    .scopes("testSc")
                    .accessTokenValiditySeconds(7200)
                    .scopes("all");
        }
    }
    
    

    使用JWT替换默认Token

    使用token方式的认证存在一个特点,就是其依赖存储,如果Redis挂了,token机制也就用不了了。
    默认的token本身只是个uuid,是不包含任何信息的。

    JWT-Json Web Token特点:

    • 是自包含的
    • 密签
    • 可扩展

    配置

    package com.imooc.security.app;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
    import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
    import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
    
    /**
     * @Author 张柳宁
     * @Description
     * @Date Create in 2018/4/23
     * @Modified By:
     */
    @Configuration
    public class TokenStoreConfig {
    
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
    
        //imooc.security.oauth2.storeType=redis时生效
        @Bean
        @ConditionalOnProperty(prefix = "imooc.security.oauth2", name = "storeType", havingValue = "redis")
        public TokenStore redisTokenStore() {
            return new RedisTokenStore(redisConnectionFactory);
        }
    
        //imooc.security.oauth2.storeType=jwt时生效,如果没有配置此项,那么也是生效的
        @Configuration
        @ConditionalOnProperty(prefix = "imooc.security.oauth2", name = "storeType", havingValue = "jwt", matchIfMissing = true)
        public static class JwtTokenConfig {
    
            @Bean
            public TokenStore jetTokenStroe() {
                return new JwtTokenStore(jwtAccessTokenConverter());
            }
    
            @Bean
            public JwtAccessTokenConverter jwtAccessTokenConverter() {
                JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
                jwtAccessTokenConverter.setSigningKey("signingjkey");//密钥,放到配置文件中
                return jwtAccessTokenConverter;
            }
        }
    }
    
    
    
    package com.imooc.security.app;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
    
    /**
     * @Author 张柳宁
     * @Description
     * @Date Create in 2018/4/23
     * @Modified By:
     */
    @Configuration
    @EnableAuthorizationServer//开启认证服务器支持
    public class ImoocAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired(required = false)
        private JwtAccessTokenConverter jwtAccessTokenConverter;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Autowired
        private TokenStore redisTokenStore;
    
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(redisTokenStore)
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService);
            if (jwtAccessTokenConverter != null) {
                endpoints.accessTokenConverter(jwtAccessTokenConverter);
            }
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()//配置内存中,也可以是数据库
                    .withClient("clientId-imooc")//clientid
                    .secret("clientSecret-imooc")
                    .accessTokenValiditySeconds(3600)//token有效时间  秒
                    .authorizedGrantTypes("refresh_token", "password", "authorization_code")//token模式
                    .scopes("all", "read", "write")//限制允许的权限配置
    
                    .and()//下面配置第二个应用   (不知道动态的是怎么配置的,那就不能使用内存模式,应该使用数据库模式来吧)
                    .withClient("test")
                    .scopes("testSc")
                    .accessTokenValiditySeconds(7200)
                    .scopes("all");
        }
    }
    
    

    配置完后,我们使用密码模式发送请求


    image.png

    我们发现,token和refresh_token就不再是uuid了。

    {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjQ1NzkyMjAsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iLCJST0xFX1VTRVIiXSwianRpIjoiYTA5YmFkZjUtYWE2ZC00NDAwLTgxY2YtMDkxYzQ5ZDEyZWY5IiwiY2xpZW50X2lkIjoiY2xpZW50SWQtaW1vb2MiLCJzY29wZSI6WyJhbGwiXX0.LtL1kcDY-396cf_o1keujKIEpDQj6BvVd5znX-ICMHI",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJhMDliYWRmNS1hYTZkLTQ0MDAtODFjZi0wOTFjNDlkMTJlZjkiLCJleHAiOjE1MjcxNjc2MjAsImF1dGhvcml0aWVzIjpbImFkbWluIiwiUk9MRV9VU0VSIl0sImp0aSI6IjQ5MThjZTFmLTQyNDktNDEyOS1hZjYwLTBlMDcxNThlNjBkMyIsImNsaWVudF9pZCI6ImNsaWVudElkLWltb29jIn0.5wfm4E2HQBZH94HaaDD_p7vr4FVtRJ1Et7F0Oo7pKqM",
    "expires_in": 3599,
    "scope": "all",
    "jti": "a09badf5-aa6d-4400-81cf-091c49d12ef9"
    }
    

    对于token的含义,我们可以在 jwt.io 网站上进行解码

    image.png

    相当于我们服务器拿到jwt形式的token后,只需要解码,就能够知道用户登陆信息,不需要去redis等存储服务器中去查找。

    使用方式与默认的token一样,在请求头中添加
    Authorization=bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjQ1ODAzMjYsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iLCJST0xFX1VTRVIiXSwianRpIjoiNzgxZjhkN2YtNGQ5NS00YzczLWI4MGEtYTYzYzBjMDBkN2VjIiwiY2xpZW50X2lkIjoiY2xpZW50SWQtaW1vb2MiLCJzY29wZSI6WyJhbGwiXX0.qVsrWRZvTw22vJQ_H-0wB1myRCvWCIQsI4iZuUq3UJQ即可

    扩展JWT信息

    这里的扩展,指的是可以在发放token的时候,添加自定义字段返回给调用方,而且调用房拿到的token中,会返回这个字段。

    • ImoocJwtTokenEnhancer
    package com.imooc.security.app.jwt;
    
    import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.security.oauth2.provider.OAuth2Authentication;
    import org.springframework.security.oauth2.provider.token.TokenEnhancer;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @Author 张柳宁
     * @Description
     * @Date Create in 2018/4/24
     * @Modified By:
     */
    
    public class ImoocJwtTokenEnhancer implements TokenEnhancer{
        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            Map<String,Object> info = new HashMap<>();
            info.put("author","张柳宁");
            ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info);
    
    
            return accessToken;
        }
    }
    
    
    • TokenStoreConfig
            @Bean
            public TokenEnhancer jwtTokenEnhancer() {
                return new ImoocJwtTokenEnhancer();
            }
    
    • ImoocAuthorizationServerConfig
        @Autowired(required = false)
        private TokenEnhancer jwtTokenEnhancer;
    
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(redisTokenStore)
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService);
            if (jwtAccessTokenConverter != null
                    && jwtTokenEnhancer != null) {
    
                TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
                List<TokenEnhancer> enhancers = new ArrayList<>();
                enhancers.add(jwtTokenEnhancer);
                enhancers.add(jwtAccessTokenConverter);
    
                enhancerChain.setTokenEnhancers(enhancers);
    
                endpoints
                        .tokenEnhancer(enhancerChain)
                        .accessTokenConverter(jwtAccessTokenConverter);
            }
        }
    

    令牌刷新

    我们一般还是会设置令牌有效时间的,如果令牌过期了就让用户重新登陆,这个体验很不好。
    在我们先前获取token的时候,都有一个refresh_token,这个就是用来刷新token的。

    image.png

    对于客户端来说可以这么使用:当获取到token的时候,查看过期时间还剩下多少,一看所剩时间不多,马上刷新,获取新的token,然后再发起调用。

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()//配置内存中,也可以是数据库
                    .withClient("clientId-imooc")//clientid
                    .secret("clientSecret-imooc")
                    .accessTokenValiditySeconds(3600)//token有效时间  秒
                    .refreshTokenValiditySeconds(3600*24*30)//refresh_token有效时间
                    .authorizedGrantTypes("refresh_token", "password", "authorization_code")//token模式
                    .scopes("all", "read", "write")//限制允许的权限配置
    
                    .and()//下面配置第二个应用   (不知道动态的是怎么配置的,那就不能使用内存模式,应该使用数据库模式来吧)
                    .withClient("test")
                    .scopes("testSc")
                    .accessTokenValiditySeconds(7200)
                    .scopes("all");
        }
    
    

    refresh_token的有效时间,我们可以设置得久一点。

    基于JWT实现SSO---单点登录

    什么是单点登陆?
    我们访问淘宝后登陆,然后跳转到天猫,发现也已经用淘宝的账号进行了登陆。

    就是说一个账号同时在多个站点中有效,且不需要重复登陆,这就是单点登陆。

    原理

    image.png

    获取当前登陆用户信息

    获取普通认证方式的用户

    不管表单认证,短信认证,token认证,只要上使用Spring Security框架进行的认证,都可以使用下面的方法获取当前登陆的用户

            UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            System.out.println(ReflectionToStringBuilder.toString(userDetails, ToStringStyle.MULTI_LINE_STYLE));
            System.out.println(userDetails.getAuthorities());
    
    

    另一种方式

        @GetMapping("/me")
        public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails){
            return userDetails;
        }
    
    

    获取JWT认证方式的用户

    如果使用的是JWT,那么获取当前用户的方式为

        @GetMapping("/me2")
        public Object getCurrentUser2(Authentication user){
            return user;
        }
    

    返回是一个json串

    {
        "authorities": [
            {
                "authority": "admin"
            },
            {
                "authority": "ROLE_USER"
            }
        ],
        "details": {
            "remoteAddress": "0:0:0:0:0:0:0:1",
            "sessionId": null,
            "tokenValue": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjQ1ODAzMjYsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iLCJST0xFX1VTRVIiXSwianRpIjoiNzgxZjhkN2YtNGQ5NS00YzczLWI4MGEtYTYzYzBjMDBkN2VjIiwiY2xpZW50X2lkIjoiY2xpZW50SWQtaW1vb2MiLCJzY29wZSI6WyJhbGwiXX0.qVsrWRZvTw22vJQ_H-0wB1myRCvWCIQsI4iZuUq3UJQ",
            "tokenType": "bearer",
            "decodedDetails": null
        },
        "authenticated": true,
        "userAuthentication": {
            "authorities": [
                {
                    "authority": "admin"
                },
                {
                    "authority": "ROLE_USER"
                }
            ],
            "details": null,
            "authenticated": true,
            "principal": "admin",
            "credentials": "N/A",
            "name": "admin"
        },
        "clientOnly": false,
        "principal": "admin",
        "oauth2Request": {
            "clientId": "clientId-imooc",
            "scope": [
                "all"
            ],
            "requestParameters": {
                "client_id": "clientId-imooc"
            },
            "resourceIds": [
            ],
            "authorities": [
            ],
            "approved": true,
            "refresh": false,
            "redirectUri": null,
            "responseTypes": [
            ],
            "extensions": {
            },
            "grantType": null,
            "refreshTokenRequest": null
        },
        "credentials": "",
        "name": "admin"
    }
    
    

    输出

    org.springframework.security.core.userdetails.User@2d3ae2b[
      password=<null>
      username=admin
      authorities=[ROLE_USER, admin]
      accountNonExpired=true
      accountNonLocked=true
      credentialsNonExpired=true
      enabled=true
    ]
    [ROLE_USER, admin]
    

    获取JWT用户及其扩展字段

            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.0</version>
            </dependency>
    

    引入依赖支持,用于解析JWT

        @GetMapping("/me2")
        public Object getCurrentUser2(Authentication user, HttpServletRequest request) throws UnsupportedEncodingException {
    
            String header = request.getHeader("Authorization");
            String token = StringUtils.substringAfter(header, "bearer ");
            Claims claims = Jwts.parser().setSigningKey("signingjkey".getBytes("UTF-8")).parseClaimsJws(token).getBody();
    
            String author = (String) claims.get("author");
    
            System.out.println("Author:" + author);
    
            return user;
        }
    

    注意,一定要在获取密签的时候指定编码,这里的author就是我们的自定义字段

    有需要spring security视频教程的同学,加我微信 。备注 来自简书


    微信号

    相关文章

      网友评论

      • 775448f2f768:画虎不成反类犬
      • 0b79024a20ea:我已经获取到了授权码,但在向 http://localhost:8080/oauth/token 发送post请求时一直弹出
        "需要进行身份认证"的弹框,输入什么都是重新弹出一样的弹框,不知该怎么解决,我的
        springboot版本为2.0.2
        Koko_滱滱:@辛勤与醉了 需要在HEADER中,根据你的client-id和client-secret配置Authorization参数
      • yangjingqzp:jsessionid 和 token 不是一样的么,如果直接通过 http header 传输?
        Koko_滱滱:@yangjingqzp 看你怎么理解了,本质上都是一个用于认证的东西。但是整个流程上是有区别的。用token的话,做了一些以前是浏览器做的工作。而且token可以自包含。大项目中使用token更多些,对于前后端分离也有好处。
        yangjingqzp:@Koko_滱滱 如果我将它通过头信息传输,不就与token一样了吗
        Koko_滱滱:@yangjingqzp 不一样,jsessionid是浏览器根据服务器返回的set-cookie维护的,通过cookie传输

      本文标题:06 Spring Security OAuth开发APP认证框

      本文链接:https://www.haomeiwen.com/subject/zdarlftx.html