一路繁花似锦绣前程
失败的越多,成功才越有价值

导航

 

一、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;
    }
}
posted on 2021-06-16 12:44  一路繁花似锦绣前程  阅读(339)  评论(0编辑  收藏  举报