SpringCloud(10) ------>微服务下的Security

一、搭建认证中心

1、新建security-server认证中心

 父工程下 new modle-->maven项目-->项目名

2、向pom文件添加依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <!--eureka客户端client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--mysql依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>

3、application.yml配置

spring:
  profiles:
    active: default

#####################客户端单节点配置###################
---
spring:
  application:
    name: security-server
  profiles: default
  #mysql
  datasource: #数据源
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/stmg?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
server:
  port: 8085 #运行端口号

eureka:
  instance:
    lease-renewal-interval-in-seconds: 5
    #续约更新时间间隔
    lease-expiration-duration-in-seconds: 15
    #续约更新时间间隔
    hostname: localhost
    #指定主机地址
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    healthcheck:
      enabled: true
    register-with-eureka: true
    #注册到Eureka的注册中心
    fetch-registry: true
    #获取注册实例列表
    service-url:
      defaultZone:  http://localhost:8761/eureka
      #配置注册中心地址
    registry-fetch-interval-seconds: 10
    # 设置服务消费者从注册中心拉取服务列表的间隔

#mybatis-plus
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.donleo.security.model
  configuration:
    map-underscore-to-camel-case: true

#log
logging:
  level:
    com.donleo.security.mapper: debug

jwt:
  #定义盐  密码
  secret: mySecret
  #过期时间(s)
  expiration: 1800
  #token 的类型 说明他以 bearer 开头
  tokenHead: bearer
  #token 对应的 key
  tokenHeader: Authorization
#  {Authorization: "bearer sdfdsfsdfsdfdsfsdfadfdsf"}

4、config配置

  1)security配置

package com.donleo.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author liangd
 * @since 2021-01-15 17:36
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and()
                .csrf().disable()
                .sessionManagement()// 基于token,所以不需要 securityContext
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/css/**", "/js/**", "/fonts/**", "/user/login", "/user/register","/user/checkAuth").permitAll() //都可以访问
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated() // 任何请求都需要认证
                .and()
                .userDetailsService(userDetailsService)
        ;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

  2)跨域拦截配置

package com.donleo.security.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * @author liangd
 * @since 2021-01-15 17:38
 */
@Configuration
@Order(1)
public class FilterConfig {
    /**
     * SpringBoot升级2.4.0 需要将addAllowedOrigin替换成.addAllowedOriginPattern
     */
    private CorsConfiguration buildConfig(){
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedHeader("*"); // 允许任何的head头部
        corsConfiguration.addAllowedOrigin("*"); // 允许任何域名使用
        corsConfiguration.addAllowedMethod("*"); // 允许任何的请求方法
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }

    /**
     * 添加CorsFilter拦截器,对任意的请求使用
     * @return CorsFilter
     */
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}

5、jwtTokenUtil工具类

package com.donleo.security.utils;


import com.donleo.security.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.stream.Collectors;

/**
 * JwtToken生成的工具类
 * JWT token的格式:header.payload.signature
 * header的格式(算法、token的类型):
 * {"alg": "HS512","typ": "JWT"}
 * payload的格式(用户名、创建时间、生成时间):
 * {"sub":"wang","created":1489079981393,"exp":1489684781}
 * signature的生成算法:
 * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
 */
@Component
public class JwtTokenUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    private static final String CLAIM_KEY_AUTHORITY = "auth";

    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 根据负责生成JWT的token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从token中获取JWT中的负载
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}", token);
        }
        return claims;
    }

    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 从token中获取登录用户名(用户编号)
     */
    public String getUserCodeFromToken(String token) {
        String userCode;
        try {
            Claims claims = getClaimsFromToken(token);
            userCode = claims.getSubject();
        } catch (Exception e) {
            userCode = null;
        }
        return userCode;
    }

    /**
     * 验证token是否还有效
     *
     * @param token       客户端传入的token
     * @param userDetails 从数据库中查询出来的用户信息
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String userCode = getUserCodeFromToken(token);
        return userCode.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 根据用户信息生成token(用户编号、时间以及权限)
     *
     * 如果采用远程调用来判断用户是否有权限,这里不需要将权限放进token中
     */
    public String generateToken(User user) {
        Map<String, Object> claims = new HashMap<>();
        Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
        Set<Object> collect = authorities.stream().filter(p -> StringUtils.hasText(p.getAuthority()))
                .map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
        claims.put(CLAIM_KEY_USERNAME, user.getCode());
        claims.put(CLAIM_KEY_CREATED, new Date());
        claims.put(CLAIM_KEY_AUTHORITY, collect);
        return generateToken(claims);
    }

    /**
     * 判断token是否可以被刷新
     */
    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     * 刷新token
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
}

6、model层

 1)User实体类实现UserDetails接口
package com.donleo.security.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Date;
import java.util.Set;

/**
 * <p>
 * 用户表
 * </p>
 *
 * @author liangd
 * @since 2020-12-21
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class User implements UserDetails {

    private static final long serialVersionUID = 1L;

    /**
     * id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 用户编号
     */
    private String code;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 权限集合
     */
    @JsonIgnoreProperties(ignoreUnknown = true)
    private Set<? extends GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    public void setAccountNonExpired(boolean accountNonExpired) {

    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {

    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {

    }

    @Override
    public boolean isEnabled() {
        if (this.dataEnable == null) {
            return false;
        }
        return "yes".equals(this.dataEnable);
    }

    public void setEnabled(boolean enabled) {

    }
}
 2)Permission实体类实现GrantedAuthority接口
package com.donleo.security.model;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;

import java.util.Date;

/**
 * <p>
 * 权限表
 * </p>
 *
 * @author liangd
 * @since 2020-12-21
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class Permission implements GrantedAuthority {

    private static final long serialVersionUID = 1L;

    /**
     * id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 权限编号
     */
    private String code;

    /**
     * 父id  0根节点
     */
    private Integer pid;

    /**
     * 权限名
     */
    private String name;

    /**
     * 权限值
     */
    private String value;

    /**
     * 类型 1菜单 2方法
     */
    private Integer type;

    /**
     * 方法路径
     */
    private String uri;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

    /**
     * 是否可用
     */
    private String dataEnable;

    @Override
    public String getAuthority() {
        return this.uri;
    }
}

7、service层,自定义UserDetailsService

package com.donleo.security.service.impl;

import com.donleo.security.model.Permission;
import com.donleo.security.model.User;
import com.donleo.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.List;

/**
 * @author liangd
 * date 2020-12-08 19:10
 * code 自定义UserDetailsService
 */
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    UserService userService;

    /**
     * 从数据库读取用户名认证
     * @param code 用户编号
     * @return UserDetails
     */
    @Override
    public UserDetails loadUserByUsername(String code) throws UsernameNotFoundException {
        User user= userService.getUserByCode(code);
        List<Permission> permissionList= userService.getPermissionsByUserCode(user.getCode());
        //获取用户拥有的权限
        HashSet<Permission> permissions = new HashSet<>(permissionList);
        user.setAuthorities(permissions);
        return user;
    }
}

8、启动类

/**
 * @author liangd
 * @since 2021-01-15 17:18
 */
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.donleo.security.mapper")
public class SecurityApp {
    public static void main(String[] args){
        SpringApplication.run(SecurityApp.class,args);
    }
}

二、zuul网关配置

(一)在网关拦截器处,从token中获取权限进行鉴权

1、向pom文件添加依赖

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

2、application.yml添加配置

jwt:
  #定义盐  密码
  secret: mySecret
  #过期时间(s)
  expiration: 1800
  #token 的类型 说明他以 bearer 开头
  tokenHead: bearer
  #token 对应的 key
  tokenHeader: Authorization

3、jwtTokenUtil工具类

  在jwt的工具类添加获取权限路径的方法

 /**
     * 从token中获取权限uri
     */
    public Set<String> getAuthsFromToken(String token) throws  Exception{
        Claims claims = getClaimsFromToken(token);
        return getAuthsFromClaims(claims);
    }

    private Set<String> getAuthsFromClaims(Claims claims) {
        Object o = claims.get(CLAIM_KEY_AUTHORITY);
        HashSet hashSet =new HashSet((ArrayList)o);
        return hashSet;
    }

4、自定义网关Filter拦截器

package com.donleo.zuul.filter;

import com.alibaba.fastjson.JSONObject;
import com.donleo.zuul.common.CommonResult;
import com.donleo.zuul.service.UserService;
import com.donleo.zuul.utils.JwtTokenUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import io.jsonwebtoken.ExpiredJwtException;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;

/**
 * 自定义网关过滤器
 *
 * @author liangd
 * @since 2021-01-12 18:52
 */
@Component
public class LogFilter extends ZuulFilter {
    /**
     * 分割成数组
     */
    @Value("#{'${pathList}'.split(',')}")
    private List<String> pathList;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserService userService;

    /**
     * 过滤器的类型。可选值有:
     * pre - 前置过滤
     * route - 路由后过滤
     * error - 异常过滤
     * post - 远程服务调用后过滤
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 同种类的过滤器的执行顺序。
     * 按照返回值的自然升序执行。
     * 值越小,级别越高
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 哪些请求会被过滤
     */
    @Override
    public boolean shouldFilter() {
   /*     RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        String requestURI = request.getRequestURI();
        //只有/api/w 开头的会被过滤
        return requestURI.startsWith("/api/w");*/
        //true 默认所有请求都会过滤
        return true;
    }

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletResponse response = requestContext.getResponse();
        response.setContentType("text/html;charset=UTF-8");
        HttpServletRequest request = requestContext.getRequest();
        String uri = request.getRequestURI();
        //哪些路径可以直接放行
        boolean a = pathList.stream().anyMatch(path -> StringUtils.contains(uri, path));
        //第一种方式,从token读取权限
        //第二种方式,远程调用security服务,将判断逻辑在security服务中判断是否有访问该路径的权限
        if (a) {
            return null;
        }
        String authorization = request.getHeader("Authorization");
        if (authorization == null) {
            requestContext.setResponseBody(JSONObject.toJSONString(CommonResult.unauthorized("未登录")));
            requestContext.setSendZuulResponse(false);
            return null;
        }
        String token = StringUtils.substring(authorization, "bearer".length()).trim();
        /************************第一种方式,从token读取权限*******************************/
       Set<String> auths = null;
        try {
            auths = jwtTokenUtil.getAuthsFromToken(token);
        } catch (Exception e) {
            // 处理token过期
            if (e instanceof ExpiredJwtException) {
                requestContext.setResponseBody(JSONObject.toJSONString(CommonResult.unauthorized("token已过期")));
                requestContext.setSendZuulResponse(false);
                return null;
            }
            e.printStackTrace();
        }
        //验证权限
        assert auths != null;
        boolean b = auths.stream().anyMatch(auth -> StringUtils.equals(auth, uri));
        if (!b) {
            requestContext.setResponseBody(JSONObject.toJSONString(CommonResult.forbidden("没有访问权限")));
            requestContext.setSendZuulResponse(false);
            return null;
        }
        return null;
     
}

(二)用OpenFeign远程调用接口,在认证中心进行鉴权

  当一个用户的权限过多,可以考虑通过远程调用接口的方式进行鉴权,这里采用OpenFeign调用接口的方式。

1、向pom文件新增依赖

      <!--声明式服务调用openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

2、service层

package com.donleo.zuul.service;

import com.donleo.zuul.common.CommonResult;
import com.donleo.zuul.config.FeignClientConfig;
import com.donleo.zuul.service.impl.UserServiceFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @author liangd
 * @since 2021-01-16 11:16
 */
@FeignClient(value = "security-server",fallbackFactory = UserServiceFallbackFactory.class,configuration = FeignClientConfig.class)
public interface UserService {

    @PostMapping("/user/checkAuth")
    CommonResult checkAuth(@RequestParam("userCode") String userCode, @RequestParam("uri") String uri);
}

3、fallbackFactory服务降级处理

package com.donleo.zuul.service.impl;

import com.donleo.zuul.common.CommonResult;
import com.donleo.zuul.service.UserService;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

/**
 * @author liangd
 * @since 2021-01-16 11:24
 */
@Component
public class UserServiceFallbackFactory implements FallbackFactory<UserService> {
    @Override
    public UserService create(Throwable throwable) {
        return (userCode, uri) -> CommonResult.failed("连接异常");
    }
}

4、自定义网关Filter拦截器

 @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletResponse response = requestContext.getResponse();
        response.setContentType("text/html;charset=UTF-8");
        HttpServletRequest request = requestContext.getRequest();
        String uri = request.getRequestURI();
        //哪些路径可以直接放行
        boolean a = pathList.stream().anyMatch(path -> StringUtils.contains(uri, path));
        //第一种方式,从token读取权限
        //第二种方式,远程调用security服务,将判断逻辑在security服务中判断是否有访问该路径的权限
        if (a) {
            return null;
        }
        String authorization = request.getHeader("Authorization");
        if (authorization == null) {
            requestContext.setResponseBody(JSONObject.toJSONString(CommonResult.unauthorized("未登录")));
            requestContext.setSendZuulResponse(false);
            return null;
        }
        String token = StringUtils.substring(authorization, "bearer".length()).trim();
  
        /************************第二种方式,远程调用security服务**************************/
        try {
            jwtTokenUtil.isTokenExpired(token);
        } catch (Exception e) {
            if (e instanceof ExpiredJwtException) {
                requestContext.setResponseBody(JSONObject.toJSONString(CommonResult.unauthorized("token已过期")));
                requestContext.setSendZuulResponse(false);
                return null;
            }
            e.printStackTrace();
        }
        String userCode = jwtTokenUtil.getUserCodeFromToken(token);
        CommonResult commonResult = userService.checkAuth(userCode, uri);
        long code = commonResult.getCode();
        if (code == 200) {
            return null;
        } else if (code == 500) {
            requestContext.setResponseBody(JSONObject.toJSONString(CommonResult.forbidden("没有访问权限")));
            requestContext.setSendZuulResponse(false);
            return null;
        } else {
            requestContext.setResponseBody(JSONObject.toJSONString(CommonResult.failed("未知错误")));
            requestContext.setSendZuulResponse(false);
            return null;
        }
    }

 5、启动类上添加注解@EnableFeignClients

 

posted @ 2021-01-16 17:20  donleo123  阅读(259)  评论(0编辑  收藏  举报