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
分类:
SpringCloud
标签:
SpringCloud
, Security
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix