zoukankan      html  css  js  c++  java
  • oauth2+spring security +jwt 完成分布式服务认证

    数据的建设可以去看 我之前的博客

    package com.aila.config;
    
    import com.aila.utils.UserJwt;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.context.SecurityContextHolder;
    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.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.oauth2.provider.ClientDetails;
    import org.springframework.security.oauth2.provider.ClientDetailsService;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    /**
     * @Author: {---chenzhichao---}
     * @Date: 2020/6/5 11:23
     */
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        ClientDetailsService clientDetailsService;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //取出身份,如果身份为空说明没有认证
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            //没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
            if(authentication==null){
                ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
                if(clientDetails!=null){
                    //秘钥
                    String clientSecret = clientDetails.getClientSecret();
                    //静态方式
                    //return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
                    //数据库查找方式
                    return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
                }
            }
    
            if (StringUtils.isEmpty(username)) {
                return null;
            }
            BCryptPasswordEncoder passwordEncoder=new BCryptPasswordEncoder();
            String encode = passwordEncoder.encode("qpalzm");
            String permissions = "salesman,accountant,user";
            UserJwt jwt = new UserJwt(username, encode, AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
            return jwt;
        }
    }

    这里对使用了BCrypt 加密方式 这里主要对数据库查询用户信息   把用户的密码和权限查出来 返回给oauth2 进行校验

    如果登录成功 应该返回如下

    {
        "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhcHAiXSwibmFtZSI6bnVsbCwiaWQiOm51bGwsImV4cCI6MTU5MTU1NDcwMSwiYXV0aG9yaXRpZXMiOlsiYWNjb3VudGFudCIsInVzZXIiLCJzYWxlc21hbiJdLCJqdGkiOiI4YmNkNzRmYS0zMWFkLTRjZDktOTJhZi05OThjMmEwMWM4NDgiLCJjbGllbnRfaWQiOiJhaWxhIiwidXNlcm5hbWUiOiJjemMifQ.cXGPAZlhCLUgI44fXHejOXjdXA8YNaQhLOLGvkSrclr4clcUqy9AKMuzW9L5ssNA_q9lCH2IuX8uAJI9WNDS0Opx4EQ54YwRE-uH4QgfoMlz_4vyAcTWuSF6OTgjxRmTSX1oXwkCr70_l0_9rrkXzGoorAECkbEPA5D_t27gDRVTI0biQ8l87PlNV3qt86c1y6X2b4vKuV16I29PyKjCBAUb9acQRehiwHPtF53gtJ_MKkh7eA0pugfK26M0KtC9t93bRVpEd1vuahVuhxPSvnsQRK5LSwml0FcW7I7CWF8GVSIHDE3VtYKfS1mTjxwoeRLOlE0GAAd_ZXDoD33WzA",
        "token_type": "bearer",
        "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhcHAiXSwiYXRpIjoiOGJjZDc0ZmEtMzFhZC00Y2Q5LTkyYWYtOTk4YzJhMDFjODQ4IiwibmFtZSI6bnVsbCwiaWQiOm51bGwsImV4cCI6MTU5MTU1NDcwMSwiYXV0aG9yaXRpZXMiOlsiYWNjb3VudGFudCIsInVzZXIiLCJzYWxlc21hbiJdLCJqdGkiOiJmNzkzMDg5Yi0xYjk1LTRiZmMtOTIwMy1iZjgwMjJjZWYwNGQiLCJjbGllbnRfaWQiOiJhaWxhIiwidXNlcm5hbWUiOiJjemMifQ.MbaFrxhg6C5z_oL1LJZsxV6HCEjE4BeYDGKHIiMwJ0hYAfl1Ad6q2bRRE_J_Jd5ByovHF_uzJTyRHjgSomNUqpJqkfcLFEFlAg-BTYbCB_19npGInMDqCqyVsUwgiza04rflONrwUxgcHtMwUJTxIe1JS5jFEsaHry55o3Gr_zQBMyg3bp4MEDtjgozBLkwq42LXDu1E3wtEOVt3jSiQaz0_Zf96P4Dj2T6t0wigRi4GUUSWyzh_V4qM1e6u3jBZC49C1oJ8la11XYLZnF03PNV1g_OGlD44zjVRfz7swBnko2A_xMxZPbQnmCgxPaX6nuev2-SUFPg2OkP6tkq38A",
        "expires_in": 43199,
        "scope": "app",
        "jti": "8bcd74fa-31ad-4cd9-92af-998c2a01c848"
    }

    当然你也可以让前端解放双手不需要每次请求一个服务都需要在请求的头部封装jwt令牌

    将jti(jwt的唯一短标识)放在用户的cookie 中 讲jwt令牌作为 string类型的values key为jti 存放在redis 中设置一个过期时间

    这样下次访问微服务只需要在网关里去判断 cookie中的jti 在redis中有没有存在就可以判断用户有没有登录  将jwt令牌手动封装在请求的头部 转发给具体的微服务

    package com.aila.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    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.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
    import org.springframework.security.oauth2.provider.ClientDetailsService;
    import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
    import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
    import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
    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.KeyStoreKeyFactory;
    
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    import java.security.KeyPair;
    
    
    @Configuration
    @EnableAuthorizationServer
    class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        //数据源,用于从数据库获取数据进行认证操作,测试可以从内存中获取
        @Autowired
        private DataSource dataSource;
        //jwt令牌转换器
        @Autowired
        private JwtAccessTokenConverter jwtAccessTokenConverter;
        //SpringSecurity 用户自定义授权认证类
        @Autowired
        UserDetailsService userDetailsService;
        //授权认证管理器
        @Autowired
        AuthenticationManager authenticationManager;
        //令牌持久化存储接口
        @Autowired
        TokenStore tokenStore;
        @Autowired
        private CustomUserAuthenticationConverter customUserAuthenticationConverter;
    
        /***
         * 客户端信息配置
         * @param clients
         * @throws Exception
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.jdbc(dataSource).clients(clientDetails());
        }
    
        /***
         * 授权服务器端点配置
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
            DefaultTokenServices tokenServices = new DefaultTokenServices();
            tokenServices.setTokenStore(endpoints.getTokenStore());
            tokenServices.setSupportRefreshToken(true);
            tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
            tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
            tokenServices.setAccessTokenValiditySeconds(60*60*2);//token有效期设置2个小时
            tokenServices.setRefreshTokenValiditySeconds(60*60*12);//Refresh_token:12个小时
    
            endpoints.accessTokenConverter(jwtAccessTokenConverter)
                    .authenticationManager(authenticationManager)//认证管理器
                    .tokenStore(tokenStore)                       //令牌存储
                    .userDetailsService(userDetailsService) //用户信息service
                    /*.tokenServices(tokenServices)*/;
        }
    
        /***
         * 授权服务器的安全配置
         * @param oauthServer
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            oauthServer.allowFormAuthenticationForClients()
                    .passwordEncoder(new BCryptPasswordEncoder())
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("isAuthenticated()")
                    ;
        }
    
    
        //读取密钥的配置
        @Bean("keyProp")
        public KeyProperties keyProperties(){
            return new KeyProperties();
        }
    
        @Resource(name = "keyProp")
        private KeyProperties keyProperties;
    
        //客户端配置
        @Bean
        public ClientDetailsService clientDetails() {
            return new JdbcClientDetailsService(dataSource);
        }
    
        @Bean
        @Autowired
        public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
            return new JwtTokenStore(jwtAccessTokenConverter);
        }
    
        /****
         * JWT令牌转换器
         * @param customUserAuthenticationConverter
         * @return
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            KeyPair keyPair = new KeyStoreKeyFactory(
                    keyProperties.getKeyStore().getLocation(),                          //证书路径 aila.jks
                    keyProperties.getKeyStore().getSecret().toCharArray())              //证书秘钥 ailapass
                    .getKeyPair(
                            keyProperties.getKeyStore().getAlias(),                     //证书别名 aila
                            keyProperties.getKeyStore().getPassword().toCharArray());   //证书密码 ailapass
            converter.setKeyPair(keyPair);
            //配置自定义的CustomUserAuthenticationConverter
            DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
            accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
            return converter;
        }
    }
    package com.aila.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    @EnableWebSecurity
    @Order(-1)
    class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        /***
         * 忽略安全拦截的URL
         * @param web
         * @throws Exception
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/oauth/login",
                    "/oauth/logout","/oauth/toLogin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");
        }
    
        /***
         * 创建授权管理认证对象
         * @return
         * @throws Exception
         */
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            AuthenticationManager manager = super.authenticationManagerBean();
            return manager;
        }
    
        /***
         * 采用BCryptPasswordEncoder对密码进行编码
         * @return
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        /****
         *
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .httpBasic()        //启用Http基本身份验证
                    .and()
                    .formLogin()       //启用表单身份验证
                    .and()
                    .authorizeRequests()    //限制基于Request请求访问
                    .anyRequest()
                    .authenticated();       //其他请求都需要经过验证
    
            //开启表单登录
            http.formLogin().loginPage("/oauth/toLogin")//设置访问登录页面的路径
                    .loginProcessingUrl("/oauth/login");//设置执行登录操作的路径
        }
    }
    server:
      port: 9200
    spring:
      application:
        name: auth2
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/class19?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
        username: root
        password: root
    auth:
      ttl: 3600  #token存储到redis的过期时间
      clientId: aila
      clientSecret: aila
      cookieDomain: localhost
      cookieMaxAge: -1
    encrypt:
      key-store:
        location: classpath:/aila.jks
        secret: ailapass
        alias: aila
        password: ailapass


    这些是oauth2 的配置

    接下来是需要jwt令牌的微服务配置

    package com.aila.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.core.io.Resource;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    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 java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.util.stream.Collectors;
    
    @Configuration
    @EnableResourceServer
    //开启方法上的PreAuthorize注解
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        //公钥
        private static final String PUBLIC_KEY = "public.key";
    
        /***
         * 定义JwtTokenStore
         * @param jwtAccessTokenConverter
         * @return
         */
        @Bean
        public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
            return new JwtTokenStore(jwtAccessTokenConverter);
        }
    
        /***
         * 定义JJwtAccessTokenConverter
         * @return
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setVerifierKey(getPubKey());
            return converter;
        }
        /**
         * 获取非对称加密公钥 Key
         * @return 公钥 Key
         */
        private String getPubKey() {
            Resource resource = new ClassPathResource(PUBLIC_KEY);
            try {
                InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
                BufferedReader br = new BufferedReader(inputStreamReader);
                return br.lines().collect(Collectors.joining("
    "));
            } catch (IOException ioe) {
                return null;
            }
        }
    
        /***
         * Http安全配置,对每个到达系统的http请求链接进行校验
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            //所有请求必须认证通过
            http.authorizeRequests()
                    .anyRequest().
                    authenticated();    //其他地址需要认证授权
        }
    }

    jwttoken 会去解析封装太头部的 jwt令牌 对他进行解密操作 如果不能解密说明 请求不可靠拒绝访问

    {
        "error": "invalid_token",
        "error_description": "Cannot convert access token to JSON"
    }
    
    
    
    401错误

    既然是security 那就可以做权限校验,在oauth中 我们已经封装了三个权限 user,salesmen,accountant

    在c层的方法上@PreAuthorize("hasAuthority('admin')") 这样 必须要有admin 权限才能访问  ,请求结果

    {
        "error": "access_denied",
        "error_description": "不允许访问"
    }
    
    错误代码403

    公钥和私钥的生成可以去百度  需要用到OpenSSL

  • 相关阅读:
    Atitit.播放系统规划新版本 v4 q18 and 最近版本回顾
    Atitit.播放系统规划新版本 v4 q18 and 最近版本回顾
    atitit.极光消息推送服务器端开发实现推送  jpush v3. 总结o7p
    atitit.极光消息推送服务器端开发实现推送  jpush v3. 总结o7p
    Atitit.文件搜索工具 attilax 总结
    Atitit.文件搜索工具 attilax 总结
    Atitit.软件命名空间  包的命名统计 及命名表(2000个名称) 方案java package
    Atitit.软件命名空间  包的命名统计 及命名表(2000个名称) 方案java package
    Atitit..状态机与词法分析  通用分词器 分词引擎的设计与实现 attilax总结
    Atitit..状态机与词法分析  通用分词器 分词引擎的设计与实现 attilax总结
  • 原文地址:https://www.cnblogs.com/kyousuke/p/13060657.html
Copyright © 2011-2022 走看看