zoukankan      html  css  js  c++  java
  • springsecurity密码模式项目实战

    一、springsecurity密码模式项目实战

    1、前言

    a、整体框架spring-cloud-alibaba-nacos + spring-security + jwt + redis
    

    2、认证服务器

    a、认证服务器pom.xml
    <!-- Spring Security、OAuth2 和JWT等 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    
    <!--redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- nacos 客户端 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
    <!-- nacos 配置中心 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    
    <!-- feign 调用服务接口 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!--springboot 2.3.x版本以上将validation单独抽取出来了,要我们自己引入-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    <!--mybatis-plus启动器-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
    <!--Druid连接池-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- 配置处理器 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    
    <!--lombok setter,getter-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
    <!-- swagger-->
    <dependency>
        <groupId>com.spring4all</groupId>
        <artifactId>swagger-spring-boot-starter</artifactId>
    </dependency>
    
    <!-- aliyun -->
    <!-- aliyun oss-->
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
    </dependency>
    
    <!--http请求工具-->
    <dependency>
        <groupId>com.arronlong</groupId>
        <artifactId>httpclientutil</artifactId>
    </dependency>
    
    <!-- 工具类依赖 -->
    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
    </dependency>
    
    b、bootstrap.yml和application.yml
    spring:
      application:
        name: auth-server # 当前服务的应用名,与nacos中的dataid的前缀匹配
      cloud:
        nacos:
          discovery:
            server-addr: 172.21.25.56:8848 # 注册中心地址  nacos server
          config:
            server-addr: 172.21.25.56:8848 # 配置中心地址 nacos server
            file-extension: yml # 配置中心的配置后缀
      profiles:
        active: dev # 指定环境为开发环境,即读取 auth-server-dev.yml
    
    server:
      port: 7001
      servlet:
        context-path: /auth # 上下文件路径,请求前缀 ip:port/article
    
    spring:
      redis:
        host: 172.21.25.56
        port: 6379
        password: # redis不需要用户名
      # 数据源配置
      datasource:
        username: root
        password: 123456
        url: jdbc:mysql://172.21.25.56:3306/dcy_blog_auth?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
        #mysql8版本以上驱动包指定新的驱动类
        driver-class-name: com.mysql.cj.jdbc.Driver
        #   数据源其他配置, 在 DruidConfig配置类中手动绑定
        initialSize: 8
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
    
    c、启动类
    @EnableFeignClients // 扫描Feign接口
    @EnableDiscoveryClient // 标识nacos客户端
    @SpringBootApplication
    public class AuthApplication {
        public static void main(String[] args) {
            SpringApplication.run(AuthApplication.class, args);
        }
    }
    
    d、安全配置类
    /**
     * 安全配置类
     */
    @EnableWebSecurity
    public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 指定使用自定义查询用户信息来完成身份认证
            auth.userDetailsService(userDetailsService);
        }
    
        @Bean // 使用 password模块时需要此bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Autowired
        private AuthenticationSuccessHandler authenticationSuccessHandler;
    
        @Autowired
        private AuthenticationFailureHandler authenticationFailureHandler;
    
        @Autowired
        private LogoutSuccessHandler logoutSuccessHandler;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 关闭csrf攻击
            http.formLogin()
                    // 成功处理器
                    // http://localhost:6001/auth/login
                    .successHandler(authenticationSuccessHandler)
                    .failureHandler(authenticationFailureHandler)
                    .and()
                    .logout()
                    // http://localhost:6001/auth/logout
                    .logoutSuccessHandler(logoutSuccessHandler)
                    .and()
                    .csrf().disable();
        }
    }
    
    @Service // 不一定不要少了
    public class UserDetailsServiceImpl implements UserDetailsService {
        @Autowired
        private IFeignSystemController feignSystemController;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 1. 判断用户名是否为空
            if (StringUtils.isEmpty(username)) {
                throw new BadCredentialsException("用户名不能为空");
            }
            // 2. 通过用户名查询数据库中的用户信息
            SysUser sysUser = feignSystemController.findUserByUsername(username);
            if (sysUser == null) {
                throw new BadCredentialsException("用户名或密码错误");
            }
    
            // 3. 通过用户id去查询数据库的拥有的权限信息
            List<SysMenu> menuList =
                    feignSystemController.findMenuListByUserId(sysUser.getId());
    
            // 4. 封装权限信息(权限标识符code)
            List<GrantedAuthority> authorities = null;
            if (CollectionUtils.isNotEmpty(menuList)) {
                authorities = new ArrayList<>();
                for (SysMenu menu : menuList) {
                    // 权限标识
                    String code = menu.getCode();
                    authorities.add(new SimpleGrantedAuthority(code));
                }
            }
    
            // 5. 构建UserDetails接口的实现类JwtUser对象
            JwtUser jwtUser = new JwtUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(),
                    sysUser.getNickName(), sysUser.getImageUrl(), sysUser.getMobile(), sysUser.getEmail(),
                    sysUser.getIsAccountNonExpired(), sysUser.getIsAccountNonLocked(),
                    sysUser.getIsCredentialsNonExpired(), sysUser.getIsEnabled(),
                    authorities);
    
            return jwtUser;
        }
    }
    
    @Data
    public class JwtUser implements UserDetails {
        @ApiModelProperty(value = "用户ID")
        private String uid;
    
        @ApiModelProperty(value = "用户名")
        private String username;
    
        @JSONField(serialize = false) // 忽略转json
        @ApiModelProperty(value = "密码,加密存储, admin/1234")
        private String password;
    
        @ApiModelProperty(value = "昵称")
        private String nickName;
    
        @ApiModelProperty(value = "头像url")
        private String imageUrl;
    
        @ApiModelProperty(value = "注册手机号")
        private String mobile;
    
        @ApiModelProperty(value = "注册邮箱")
        private String email;
    
        // 1 true 0 false
        @JSONField(serialize = false) // 忽略转json
        @ApiModelProperty(value = "帐户是否过期(1 未过期,0已过期)")
        private boolean isAccountNonExpired; // 不要写小写 boolean
    
        @JSONField(serialize = false) // 忽略转json
        @ApiModelProperty(value = "帐户是否被锁定(1 未过期,0已过期)")
        private boolean isAccountNonLocked;
    
        @JSONField(serialize = false) // 忽略转json
        @ApiModelProperty(value = "密码是否过期(1 未过期,0已过期)")
        private boolean isCredentialsNonExpired;
    
        @JSONField(serialize = false) // 忽略转json
        @ApiModelProperty(value = "帐户是否可用(1 可用,0 删除用户)")
        private  boolean isEnabled;
    
        /**
         * 封装用户拥有的菜单权限标识
         */
        @JSONField(serialize = false) // 忽略转json
        private List<GrantedAuthority> authorities;
    
        //    isAccountNonExpired 是 Integer 类型接收,然后转 boolean
        public JwtUser(String uid, String username, String password,
                       String nickName, String imageUrl, String mobile, String email,
                       Integer isAccountNonExpired, Integer isAccountNonLocked,
                       Integer isCredentialsNonExpired, Integer isEnabled,
                       List<GrantedAuthority> authorities) {
            this.uid = uid;
            this.username = username;
            this.password = password;
            this.nickName = nickName;
            this.imageUrl = imageUrl;
            this.mobile = mobile;
            this.email = email;
            this.isAccountNonExpired = isAccountNonExpired == 1 ? true: false;
            this.isAccountNonLocked = isAccountNonLocked == 1 ? true: false;
            this.isCredentialsNonExpired = isCredentialsNonExpired == 1 ? true: false;
            this.isEnabled = isEnabled == 1 ? true: false;
            this.authorities = authorities;
        }
    }
    
    /**
     * 失败处理器:认证失败后响应json给前端
     */
    @Component("customAuthenticationFailureHandler")
    public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request,
                                            HttpServletResponse response,
                                            AuthenticationException e) throws IOException, ServletException {
            // 响应错误信息:json格式
            response.setContentType("application/json;charset=UTF-8");
            String result = objectMapper.writeValueAsString(Result.error(e.getMessage()));
            response.getWriter().write(result);
        }
    }
    
    /**
     * 成功处理器,校验客户端信息、生成jwt令牌,响应result数据
     */
    @Component("customAuthenticationSuccessHandler")
    public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        Logger logger = LoggerFactory.getLogger(getClass());
    
        private static final String HEADER_TYPE = "Basic ";
    
        @Autowired
        private ClientDetailsService clientDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private AuthorizationServerTokenServices authorizationServerTokenServices;
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                                            HttpServletResponse response,
                                            Authentication authentication) throws IOException, ServletException {
            logger.info("登录成功 {}", authentication.getPrincipal());
            // 获取请求头中的客户端信息
            String header = request.getHeader(HttpHeaders.AUTHORIZATION);
    
            logger.info("header {}", header);
    
            // 响应结果对象
            Result result = null;
    
            try {
                if (header == null || !header.startsWith(HEADER_TYPE)) {
                    throw new UnsupportedOperationException("请求头中无client信息");
                }
                // 解析请求头的客户端信息
                String[] tokens = RequestUtil.extractAndDecodeHeader(header);
                assert tokens.length == 2;
    
                String clientId = tokens[0];
                String clientSecret = tokens[1];
    
                // 查询客户端信息,核对是否有效
                ClientDetails clientDetails =
                        clientDetailsService.loadClientByClientId(clientId);
                if (clientDetails == null) {
                    throw new UnsupportedOperationException("clientId对应的配置信息不存在:" + clientId);
                }
                // 校验客户端密码是否有效
                if (!passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
                    throw new UnsupportedOperationException("无效clientSecret");
                }
    
                // 组合请求对象,去获取令牌
                TokenRequest tokenRequest =
                        new TokenRequest(MapUtils.EMPTY_MAP, clientId,
                                clientDetails.getScope(), "custom");
    
                OAuth2Request oAuth2Request =
                        tokenRequest.createOAuth2Request(clientDetails);
    
                OAuth2Authentication oAuth2Authentication =
                        new OAuth2Authentication(oAuth2Request, authentication);
    
                // 获取 访问令牌对象
                OAuth2AccessToken accessToken =
                        authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
    
                result = Result.ok(accessToken);
            } catch (Exception e) {
                logger.error("认证成功处理器异常={}", e.getMessage(), e);
                result = Result.build(ResultEnum.AUTH_FAIL.getCode(), e.getMessage());
            }
    
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(result));
        }
    }
    
    @Configuration
    @EnableAuthorizationServer // 标识为认证服务器
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private DataSource dataSource;
    
        @Bean // 客户端使用jdbc管理
        public ClientDetailsService jdbcClientDetailsService() {
            return new JdbcClientDetailsService(dataSource);
        }
    
        /**
         * 配置被允许访问认证服务的客户端信息:数据库方式管理客户端信息
         *
         * @param clients
         * @throws Exception
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.withClientDetails(jdbcClientDetailsService());
        }
    
        @Autowired // 在SpringSecurityConfig中已经添加到容器中了
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Resource
        private TokenStore tokenStore;
    
        @Resource
        private JwtAccessTokenConverter jwtAccessTokenConverter;
    
        @Resource // 注入增强器
        private TokenEnhancer jwtTokenEnhancer;
    
        /**
         * 关于认证服务器端点配置
         *
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            // /oauth/token post
            // Basic Auth {username,password}客户端账号密码
            // form-data {grant_type:password,username,password}密码认证模式,用户账号密码
            // ********
            // form-data {grant_type:refresh_token,refresh_token}刷新令牌
            // 密码模块必须使用这个authenticationManager实例
            endpoints.authenticationManager(authenticationManager);
            // 刷新令牌需要 使用userDetailsService
            endpoints.userDetailsService(userDetailsService);
            // 令牌管理方式
            endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
    
            // 添加增强器
            TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
            // 组合 增强器和jwt转换器
            List<TokenEnhancer> enhancerList = new ArrayList<>();
            enhancerList.add(jwtTokenEnhancer);
            enhancerList.add(jwtAccessTokenConverter);
            enhancerChain.setTokenEnhancers(enhancerList);
            endpoints.tokenEnhancer(enhancerChain).accessTokenConverter(jwtAccessTokenConverter);
        }
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            // /oauth/check_token post
            // Basic Auth {username,password}客户端账号密码
            // form-data {token}
            // 解析令牌,默认情况 下拒绝访问
            security.checkTokenAccess("permitAll()");
        }
    }
    
    @Configuration
    public class PasswordEncoderConfig {
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    
    public class RequestUtil {
        public static String[] extractAndDecodeHeader(String header) throws IOException {
            // `Basic ` 后面开始截取 clientId:clientSecret
            byte[] base64Token = header.trim().substring(6).getBytes(StandardCharsets.UTF_8);
    
            byte[] decoded;
            try {
                decoded = Base64.getDecoder().decode(base64Token);
            } catch (IllegalArgumentException var8) {
                throw new RuntimeException("请求头解析失败:" + header);
            }
    
            String token = new String(decoded, "UTF-8");
            int delim = token.indexOf(":");
            if (delim == -1) {
                throw new RuntimeException("请求头无效:" + token);
            } else {
                return new String[]{token.substring(0, delim), token.substring(delim + 1)};
            }
        }
    }
    
    /**
     * 退出成功处理器,清除redis中的数据
     */
    @Component("customLogoutSuccessHandler")
    public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    
        @Autowired
        private TokenStore tokenStore;
    
        @Override
        public void onLogoutSuccess(HttpServletRequest request,
                                    HttpServletResponse response,
                                    Authentication authentication) throws IOException, ServletException {
            // 获取访问令牌
            String accessToken = request.getParameter("accessToken");
            if (StringUtils.isNotBlank(accessToken)) {
                OAuth2AccessToken oAuth2AccessToken =
                        tokenStore.readAccessToken(accessToken);
                if (oAuth2AccessToken != null) {
                    // 删除redis中对应的访问令牌
                    tokenStore.removeAccessToken(oAuth2AccessToken);
                }
            }
    
            // 退出成功,响应结果
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(Result.ok().toJsonString());
        }
    }
    
    @Configuration
    public class JwtTokenStoreConfig {
    
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter =
                    new JwtAccessTokenConverter();
            // 采用非对称加密文件
            KeyStoreKeyFactory factory = new KeyStoreKeyFactory(
                    new ClassPathResource("oauth2.jks"), "oauth2".toCharArray());
            converter.setKeyPair(factory.getKeyPair("oauth2"));
    
            return converter;
        }
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Bean
        public TokenStore tokenStore() {
            // 采用jwt管理信息
            return new JwtTokenStore(jwtAccessTokenConverter()){
                @Override
                public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
                    // 将jwt中的唯一标识 jti 作为redis中的 key 值 ,value值是 accessToken 访问令牌
                    if(token.getAdditionalInformation().containsKey("jti")) {
                        String jti = token.getAdditionalInformation().get("jti").toString();
                        // 存储到redis中 (key, value, 有效时间,时间单位)
                        redisTemplate.opsForValue()
                                .set(jti, token.getValue(), token.getExpiresIn(), TimeUnit.SECONDS);
                    }
                    super.storeAccessToken(token, authentication);
                }
    
                @Override
                public void removeAccessToken(OAuth2AccessToken token) {
                    if(token.getAdditionalInformation().containsKey("jti")) {
                        String jti = token.getAdditionalInformation().get("jti").toString();
                        // 将redis中对应jti的记录删除
                        redisTemplate.delete(jti);
                    }
                    super.removeAccessToken(token);
                }
            };
        }
    }
    
    /**
     * 扩展响应的认证信息
     */
    @Component
    public class JwtTokenEnhancer implements TokenEnhancer {
    
        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken,
                                         OAuth2Authentication oAuth2Authentication) {
            JwtUser user = (JwtUser) oAuth2Authentication.getPrincipal();
            Map<String, Object> map = new HashMap<>();
            map.put("userInfo", JSON.toJSON(user));
    
            // 设置附加信息
            ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(map);
    
            return oAuth2AccessToken;
        }
    }
    
    e、中文认证提示信息
    @Configuration
    public class ReloadMessageConfig {
    
        /**
         * 加载中文的认证提示信息
         *
         * @return
         */
        @Bean
        public ReloadableResourceBundleMessageSource messageSource() {
            ReloadableResourceBundleMessageSource messageSource =
                    new ReloadableResourceBundleMessageSource();
            messageSource.setBasename("classpath:messages_zh_CN"); // 不要有后缀名.properties
            return messageSource;
        }
    }
    
    f、刷新令牌
    @RestController
    public class AuthController {
        Logger logger = LoggerFactory.getLogger(getClass());
    
        private static final String HEADER_TYPE = "Basic ";
    
        @Autowired
        private ClientDetailsService clientDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private AuthService authService;
    
        @GetMapping("/user/refreshToken") // localhost:7001/auth/user/refreshToken?refreshToken=xxxx
        public Result refreshToken(HttpServletRequest request) {
            try {
                // 获取请求中的刷新令牌
                String refreshToken = request.getParameter("refreshToken");
    
                Preconditions.checkArgument(StringUtils.isNotEmpty(refreshToken), "刷新令牌不能为空");
    
                // 获取请求头
                String header = request.getHeader(HttpHeaders.AUTHORIZATION);
                if(header == null || !header.startsWith(HEADER_TYPE)) {
                    throw new UnsupportedOperationException("请求头中无client信息");
                }
                // 解析请求头的客户端信息
                String[] tokens = RequestUtil.extractAndDecodeHeader(header);
                assert tokens.length == 2;
    
                String clientId = tokens[0];
                String clientSecret = tokens[1];
    
                // 查询客户端信息,核对是否有效
                ClientDetails clientDetails =
                        clientDetailsService.loadClientByClientId(clientId);
                if(clientDetails == null) {
                    throw new UnsupportedOperationException("clientId对应的配置信息不存在:" + clientId);
                }
                // 校验客户端密码是否有效
                if( !passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
                    throw new UnsupportedOperationException("无效clientSecret");
                }
    
                // 获取新的认证信息
                return authService.refreshToken(header, refreshToken);
            } catch(Exception e) {
                logger.error("refreshToken={}", e.getMessage(), e);
                return Result.error("新令牌获取失败:" + e.getMessage());
            }
        }
    }
    
    @Service // 不要少了
    public class AuthService {
        @Autowired // 负载均衡的client
        private LoadBalancerClient loadBalancerClient;
    
        /**
         * 通过刷新令牌获取新的认证令牌
         * @param header 请求头:客户端信息,Basic clientId:clientSecret
         * @param refreshToken 刷新令牌
         * @return
         */
        public Result refreshToken(String header, String refreshToken) throws HttpProcessException {
            // 采用客户端负载均衡,从Nacos服务器中获取对应服务的ip与端口号
            ServiceInstance serviceInstance = loadBalancerClient.choose("auth-server");
            if(serviceInstance == null) {
                return Result.error("未找到有效认证服务器,请稍后重试");
            }
            // 请求刷新令牌url
            String refreshTokenUrl = serviceInstance.getUri().toString() + "/auth/oauth/token";
    
            // 封装刷新令牌请求参数
            Map<String, Object> map = new HashMap<>();
            map.put("grant_type", "refresh_token");
            map.put("refresh_token", refreshToken);
    
            // 构建配置请求头参数
            Header[] headers = HttpHeader.custom() // 自定义请求
                    .contentType(HttpHeader.Headers.APP_FORM_URLENCODED) // 数据类型
                    .authorization(header) // 认证请求头(客户信息)
                    .build();
            // 请求配置
            HttpConfig config =
                    HttpConfig.custom().headers(headers).url(refreshTokenUrl).map(map);
    
            // 发送请求,响应认证信息
            String token = HttpClientUtil.post(config);
    
            JSONObject jsonToken = JSON.parseObject(token);
            // 如果响应内容中包含了error属性值,则获取新的认证失败。
            if(StringUtils.isNotEmpty(jsonToken.getString("error"))) {
                return Result.build(ResultEnum.TOKEN_PAST);
            }
    
            return Result.ok(jsonToken);
        }
    }
    

    3、资源服务器

    a、资源服务器pom.xml
    <!-- Spring Security、OAuth2 和JWT等-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    
    b、资源服务器配置类
    @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别权限控制
    @EnableResourceServer // 标识为资源服务器,请求资源接口时,必须在请求头带个access_token
    @Configuration
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        @Autowired
        private TokenStore tokenStore;
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.tokenStore(tokenStore); // jwt管理令牌
        }
    
        /**
         * 资源服务器的安全配置
         *
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.sessionManagement() // 采用token进行管理身份,而没有采用session,所以不需要创建HttpSession
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests() // 请求的授权配置
                    // 将 swagger接口文档相关的url放行
                    .antMatchers("/v2/api-docs", "/v2/feign-docs",
                            "/swagger-resources/configuration/ui",
                            "/swagger-resources", "/swagger-resources/configuration/security",
                            "/swagger-ui.html", "/webjars/**").permitAll()
                    // 放行以 /api 开头的请求接口
                    .antMatchers("/api/**").permitAll()
    
                    // 所有请求都要有all范围权限
                    .antMatchers("/**").access("#oauth2.hasScope('all')")
                    // 其他请求都要通过身份认证
                    .anyRequest().authenticated();
        }
    }
    
    @Configuration
    public class JwtTokenStoreConfig {
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            // 采用的是非对称加密,资源服务器要使用公钥解密 public.txt
            ClassPathResource resource = new ClassPathResource("public.txt");
            String publicKey = null;
            try {
                publicKey = IOUtils.toString(resource.getInputStream(), "UTF-8");
            } catch (IOException e) {
                e.printStackTrace();
            }
            converter.setVerifierKey(publicKey);
    
            // 将定义的转换器对象添加到jwt转换器中
            converter.setAccessTokenConverter( new CustomAccessTokenConverter() );
            return converter;
        }
    
        @Bean
        public TokenStore tokenStore() {
            return new JwtTokenStore( jwtAccessTokenConverter() );
        }
    
        /**
         * 定制 AccessToken 转换器,为额外添加的用户信息在资源服务中获取
         */
        private class CustomAccessTokenConverter extends DefaultAccessTokenConverter {
            @Override
            public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
                OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
                oAuth2Authentication.setDetails(map);
                return oAuth2Authentication;
            }
        }
    }
    
    c、解决远程调用请求头丢失问题
    /**
     * 使用 Feign进行远程调用时,先经过此拦截器,在此拦截器中将请求头带上访问令牌
     */
    @Component
    public class FeignRequestInterceptor implements RequestInterceptor {
        @Override
        public void apply(RequestTemplate requestTemplate) {
            // 通过RequestContextHolder工具来获取请求相关变量
            ServletRequestAttributes attributes =
                    (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
            if(attributes != null) {
                // 获取请求对象
                HttpServletRequest request = attributes.getRequest();
                String token = request.getHeader(HttpHeaders.AUTHORIZATION);
                if(StringUtils.isNotEmpty(token)) { // Bearer xxx
                    // 在使用feign远程调用时,请求头就会带上访问令牌
                    requestTemplate.header(HttpHeaders.AUTHORIZATION, token);
                }
            }
        }
    }
    
    d、获取当前用户
    public class AuthUtil {
        public static SysUser getUserInfo() {
            Authentication authentication
                    = SecurityContextHolder.getContext().getAuthentication();
            OAuth2AuthenticationDetails details =
                    (OAuth2AuthenticationDetails)authentication.getDetails();
            Map<String, Object> map = (Map<String, Object>) details.getDecodedDetails();
            Map<String, String>  userInfo = (Map<String, String>) map.get("userInfo");
    
            SysUser user = new SysUser();
            user.setId(userInfo.get("uid"));
            user.setNickName(userInfo.get("nickName"));
            user.setUsername( userInfo.get("username") );
            user.setEmail( userInfo.get("email") );
            user.setImageUrl( userInfo.get("imageUrl") );
            user.setMobile( userInfo.get("mobile"));
    
            return user;
        }
    }
    

    4、网关服务器

    a、网关服务器pom.xml
    <!-- 解析 jwt -->
    <dependency>
    	<groupId>com.nimbusds</groupId>
    	<artifactId>nimbus-jose-jwt</artifactId>
    	<version>6.0</version>
    </dependency>
    
    b、根据请求头是否存在认证信息判断请求是否放行
    @Component // 不要少了
    public class AuthenticationFilter implements GlobalFilter, Ordered {
    
        private static final String[] white = {"/api/"};
    
        /**
         * 定义验证请求头是否带有 Authorization
         *
         * @param exchange
         * @param chain
         * @return
         */
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 请求对象
            ServerHttpRequest request = exchange.getRequest();
            // 响应对象
            ServerHttpResponse response = exchange.getResponse();
            // /question/api/question/1
            String path = request.getPath().pathWithinApplication().value();
    
            // 公开api接口进行放行,无需认证
            if (StringUtils.indexOfAny(path, white) != -1) {
                // 直接放行
                return chain.filter(exchange);
            }
    
            // 请求头信息
            String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
            if (StringUtils.isEmpty(authorization)) {
                // 没有带authorization请求头,则响应错误信息
                // 封装响应信息
                JSONObject message = new JSONObject();
                message.put("code", 1401);
                message.put("message", "缺少身份凭证");
    
                // 转换响应消息内容对象为字节
                byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
                DataBuffer buffer = response.bufferFactory().wrap(bits);
                // 设置响应对象状态码 401
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                // 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
                response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
                // 返回响应对象
                return response.writeWith(Mono.just(buffer));
            }
            // 如果请求头不为空,则验证通过,放行此过滤器
            return chain.filter(exchange);
        }
    
    
        @Override
        public int getOrder() {
            //过滤器执行顺序,越小越优先执行
            return 0;
        }
    }
    
    c、根据redis中token存储状态判断请求是否放行
    @Component // 不要少了
    public class AccessTokenFilter implements GlobalFilter, Ordered {
        Logger logger = LoggerFactory.getLogger(getClass());
    
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
        /**
         * 校验请求头中的令牌是否有效,查询redis中是否存在 ,不存在则无效jwt
         *
         * @param exchange
         * @param chain
         * @return
         */
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 请求对象
            ServerHttpRequest request = exchange.getRequest();
            // 响应对象
            ServerHttpResponse response = exchange.getResponse();
    
            // 获取token
            String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
            String token = StringUtils.substringAfter(authorization, "Bearer ");
    
            if (StringUtils.isEmpty(token)) {
                // 如果为空,可能是白名单的请求,则直接放行
                return chain.filter(exchange);
            }
            // 响应错误信息
            String message = null;
    
            try {
                JWSObject jwsObject = JWSObject.parse(token);
                JSONObject jsonObject = jwsObject.getPayload().toJSONObject();
    
                // 校验redis中是否存在对应jti的token
                String jti = jsonObject.get("jti").toString();
                // 查询是否存在
                Object value = redisTemplate.opsForValue().get(jti);
                if (value == null) {
                    logger.info("令牌已过期 {}", token);
                    message = "您的身份已过期, 请重新认证!";
                }
    
            } catch (ParseException e) {
                logger.error("解析令牌失败 {}", token);
                message = "无效令牌";
            }
    
            if (message == null) {
                // 如果令牌存在,则通过
                return chain.filter(exchange);
            }
    
            // 响应错误提示信息
            JSONObject result = new JSONObject();
            result.put("code", 1401);
            result.put("message", message);
    
            // 转换响应消息内容对象为字节
            byte[] bits = result.toJSONString().getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = response.bufferFactory().wrap(bits);
            // 设置响应对象状态码 401
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            // 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
            response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
            // 返回响应对象
            return response.writeWith(Mono.just(buffer));
        }
    
        @Override
        public int getOrder() {
            // 这个AccessTokenFilter过滤器在 AuthenticationFilter 之后执行
            return 10;
        }
    }
    
  • 相关阅读:
    DBA手记(学习)-library cache pin
    DBA手记(学习)-RAC环境下GES TX报警情况处理
    DBA手记(学习)
    flashback query闪回数据
    在CentOS7上安装MySQL5.7-源码包方式
    PL/SQL注册码
    TortoiseGit的安装与配置
    Git安装配置及第一次上传项目到GitHub
    【IDEA使用技巧】(5) —— IntelliJ IDEA集成Tomcat部署Maven Web项目
    【IDEA使用技巧】(4) —— IDEA 构建Java Maven项目、导入Eclipse项目、多Module Maven项目
  • 原文地址:https://www.cnblogs.com/linding/p/14888933.html
Copyright © 2011-2022 走看看