两大话题
用户认证
授权
有状态&无状态
在单体架构的时代,应用常常通过session保持会话,通过将session保存到中央存储中去。常常会使用redis或memcached。在那个年代,要想搭建应用的集群,常常要借用tomcat的session共享插件或者借助sping session这样的小项目把应用的session存储到session store,以及从session store中查询session,这种方式我们认为是有状态的,因为服务端要记录用户的session.随着应用的发展,特别是近几年,微服务流行,越来越提到无状态,也就是服务端不再维护session。一个系统包括了很多的微服务,一个微服务也可能是集群。使用一个sesson store与微服务分而治之的思想是背道而驰的,如果session store挂了,那么不管有多少个微服务,系统全完蛋了,另外,如果我们session store要做迁移的话也很麻烦,因为所有的微服务都连接了这个session store,那么所有微服务都要去修改这个配置。也就是牵一发而动全身。第三,如果sessin store达到了容量瓶颈,那我们就需要扩容了。
那么无状态是怎么玩的呢?
在无状态的世界,服务器端一般不会存储用户的登录状态,而是在用户登录上颁发一个token,这个token一般是加密的,以后用户的每个请求都会带上这个token,可以存放在头信息中或者url参数中,服务端拿到token,解密一下,验证是否合法,是否过期,如果验证没问题,则认为用户没有问题。
无论是有状态还是无状态,session或是token本质上作用都是一样的,都是判断用户的一个凭证,只是无状态下服务器端只是对token做一个校验,而不需要进行存储。服务端不需要存储token,提高了微服务的伸缩。但是对于设置用户的登录有效期就比较难了。通过session可以很方便实现强制下线、登录有效期这个问题。
因此对于有状态,服务器端控制力强,但是存在中心点、鸡蛋在一个篮子里;迁移麻烦;服务器端存储数据,加大了服务器端压力。
而对于无状态去中心化,无存储、简单,任意扩容、缩容。但是缺点就是服务器端控制力相对弱。
目前无状态是越来越流行,但是也要结合实际的项目选择方案。
登录认证方案与选择
流向的微服务登录认证方案
认证方案1 - “处处安全”
https://www.cnblogs.com/cjsblog/p/10548022.html
OAuth2.0系列文章:
http://ifeve.com/oauth2-tutorial-all/
代表实现:
Spring Cloud Security:https://cloud.spring.io/spring-cloud-security/reference/html/
Jboss Keycloak : https://www.keycloak.org/
Spring Cloud Security认证授权实例代码:
https://github.com/chengjiansheng/cjs-oauth2-sso-demo
Keycloak认证授权实例代码:(基于servlet开发)
https://github.com/eacdy/spring-cloud-yes
优点:安全性高
缺点:实现成本高
这种方案此处不展开了
认证方案2 - 外部无状态,内部有状态
在这个方案下,网关不存放session,而是使用token,而网关代理点微服务使用session store共享session,看起来这种方案是很奇葩的,但是在实际中很多企业采用这种方案。这很奇怪,这种方案既没有利用到session的优势,也没有利用到无状态的优势,然后还比单纯的有状态或无状态方案还复杂。
如果你有一个很庞大的系统,早期采用了传统的架构,已经使用了session,但是现在微服务架构又采用token.
用户携带token和JSESSIONID请求API网关,网关做转发,老应用拿JSESSIONID到Session Store中查,如果能够查到,则用户已经登录了。对于微服务那一块,则解密token,验证用户是否登录,这样,就可以逐步进行重构,每次只重构遗留系统的一部分。
这种方案好像没啥优点,缺点一大点,但是可以逐步改造老项目,这是一个亮点。
认证方案3 - “ 内部裸奔”
这种方案是这样的,请求在网关上做登录认证,如果登录认证,登录成功网关就是颁发token,之后用户的每次请求都会携带这个token,网关会解密这个token,另外可以在token中存放用户的信息,网关可以解析token,获取这个信息,网关就知道是谁登录了。之后,网关会把解析出来的用户信息写到请求的http header信息中。在这种方案下,登录认证、token解密、token解析都是由网关去做的,并且网关告诉后端微服务用户是谁,微服务选择无条件去相信。优点是性能非常好,实现简单,只需要网关上添加过滤器工厂实现登录以及token解密,从而判断当前用户是否登录,以及token解析,获取到token中的信息。缺点是,网关一旦被攻破,那么用户的授权登录就形同虚设
认证方案4 - 内部裸奔 改进方案
请求经过网关,转发用户中心登录,如果登录成功,由用户中心颁发Token,以后用户的请求都会携带这个Token,网关不会操作这个token,而是发给后方的微服务,由微服务进行解密和解析Token,微服务之间调用也是传递Token,然后下一个微服务进行解密和解析。这种方案网关实现简单,不需要关注用户是谁这种强业务,另外微服务自己进行认证授权可以降低团队的沟通成本。这个方案提高了系统的安全性。但是安全性是相对的,每个微服务都需要做解密解析,知道密钥的就更多,导致密钥泄露的概率也就越大。所以使用这种方案要防止密钥泄露,可以定期更换密钥,不要开发者看到密钥本身等。优点是实现不负责,网关的工作简单,降低了团队的沟通成本,缺点是密钥一旦泄露,那就玩完了,但是这也是可以解决的。
以上的方案可以做出各种变种。
如何选择
方案 | 复杂度 | 安全性 | 性能 | 测试难度 |
---|---|---|---|---|
处处安全 | 高 | 高 | 性能中等 | 难(一般做集成测试) |
外部无状态 内部有状态 | 低 | 中 | 高 | 难(一般做集成测试) |
内部裸奔 | 低 | 一般 | 高 | 简单(造Header即可实现接口测试) |
内部裸奔改进版 | 低 | 高 | 高 | 中(造Token即可实现接口测试) |
访问控制
所谓访问控制就是满足什么样的条件可以访问,我们也可以称之为授权。目前业界比较流行的访问控制方案有:
Access Control List(ACL)
Role-based access control (RBAC) – 最流行的模型 下面也以这种进行开展
Attribute-based access control(ABAC)
Rule-based access control
Time-based access control
JWT
这是一种比较流行的token,全称Json Web Token,是一个开放标准(RFC 7519),用来在各方之间安全地传输信息.JWT可被验证和信任,因为它是数字签名的。
JWT组成
组成 | 作用 | 内容示例 |
---|---|---|
Header(头) | 记录令牌类型、签名的算法等 | {“alg”:“HS256”,“typ”:“JWT”} |
Payload(有效载荷) | 携带一些用户信息 | {“userId”:“1”,“username”:“damu”} |
Signature(签名) | 防止token被篡改、确保安全性 | 计算出来的签名,一个字符串 |
用户微服务添加如下依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
添加工具类
package com.cloud.msuser.jwt;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
@Component
public class JwtOperator {
public static final Logger log = LoggerFactory.getLogger(JwtOperator.class);
/**
* 秘钥 - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
*/
@Value("${secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
private String secret;
/**
* 有效期,单位秒 - 默认2周
*/
@Value("${expire-time-in-second:1209600}")
private Long expirationTimeInSecond;
/**
* 从token中获取claim
*
* @param token token
* @return claim
*/
public Claims getClaimsFromToken(String token) {
try {
return Jwts.parser().setSigningKey(this.secret.getBytes()).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
log.error("token解析错误", e);
throw new IllegalArgumentException("Token invalided.");
}
}
/**
* 获取token的过期时间
*
* @param token token
* @return 过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token).getExpiration();
}
/**
* 判断token是否过期
*
* @param token token
* @return 已过期返回true,未过期返回false
*/
private Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 计算token的过期时间
*
* @return 过期时间
*/
private Date getExpirationTime() {
return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
}
/**
* 为指定用户生成token
*
* @param claims 用户信息
* @return token
*/
public String generateToken(Map<String, Object> claims) {
Date createdTime = new Date();
Date expirationTime = this.getExpirationTime();
byte[] keyBytes = secret.getBytes();
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
return Jwts.builder().setClaims(claims).setIssuedAt(createdTime).setExpiration(expirationTime)
// 你也可以改用你喜欢的算法
// 支持的算法详见:https://github.com/jwtk/jjwt#features
.signWith(key, SignatureAlgorithm.HS256).compact();
}
/**
* 判断token是否非法
*
* @param token token
* @return 未过期返回true,否则返回false
*/
public Boolean validateToken(String token) {
return !isTokenExpired(token);
}
public static void main(String[] args) {
// 1. 初始化
JwtOperator jwtOperator = new JwtOperator();
jwtOperator.expirationTimeInSecond = 1209600L;
jwtOperator.secret = "aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt";
// 2.设置用户信息
HashMap<String, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("id", "1");
// 测试1: 生成token
String token = jwtOperator.generateToken(objectObjectHashMap);
// 会生成类似该字符串的内容:
// eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1ODI5ODA5MjIsImV4cCI6MTU4NDE5MDUyMn0.ldBmVbqqWNbySHk-nK4ew1Laf20LB_ok6P739keAApE
System.out.println(token);
// 将我改成上面生成的token!!!
String someToken = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1ODI5ODA5MjIsImV4cCI6MTU4NDE5MDUyMn0.ldBmVbqqWNbySHk-nK4ew1Laf20LB_ok6P739keAApE";
// 测试2: 如果能token合法且未过期,返回true
Boolean validateToken = jwtOperator.validateToken(someToken);
System.out.println(validateToken); // true
// 测试3: 获取用户信息
Claims claims = jwtOperator.getClaimsFromToken(someToken);
System.out.println(claims); // {id=1, iat=1582980922, exp=1584190522}
// 将我改成你生成的token的第一段(以.为边界)
String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";
// 测试4: 解密Header
byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
System.out.println(new String(header)); // {"alg":"HS256"}
// 将我改成你生成的token的第二段(以.为边界)
String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE1ODI5ODA5MjIsImV4cCI6MTU4NDE5MDUyMn0";
// 测试5: 解密Payload
byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
System.out.println(new String(payload)); // {"id":"1","iat":1582980922,"exp":1584190522}
// 测试6: 这是一个被篡改的token,因此会报异常,说明JWT是安全的
jwtOperator.validateToken("aeyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1ODI5ODA5MjIsImV4cCI6MTU4NDE5MDUyMn0.ldBmVbqqWNbySHk-nK4ew1Laf20LB_ok6P739keAApE");
}
}
添加配置
jwt:
# 密钥
secret: aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
# 有效期,单位秒,默认2周
expire-time-in-second: 1209600
针对课程微服务 执行同样的操作 添加依赖 添加工具类 添加配置 注意 课程微服务与用户微服务使用的密钥必须是一样的 否则一个微服务颁发的token另一个微服务就没法使用了。
实现登录认证
创建dto对象
package com.cloud.msuser.domain.dto;
public class UserLoginDTO {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
添加jpa方法findByUsernameAndPassword
package com.cloud.msuser.repository;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.cloud.msuser.domain.entity.User;
@Repository
public interface UserRepository extends CrudRepository<User, Integer> {
Optional<User> findByUsernameAndPassword(String username, String password);
}
在用户微服务的UserController中添加方法login:
package com.cloud.msuser.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.cloud.msuser.domain.dto.UserLoginDTO;
import com.cloud.msuser.domain.entity.User;
import com.cloud.msuser.service.UserService;
@RestController
public class UserController {
@Autowired
private UserService userService;
/**
* http://localhost:8081/users/1
* @param id
* @return
*/
@GetMapping("/users/{id}")
public User findById(@PathVariable Integer id) {
return this.userService.findById(id);
}
@PostMapping("/login")
public String login(@RequestBody UserLoginDTO loginDTO) {
return this.userService.login(loginDTO);
}
}
在UserService中添加方法
@Autowired
private JwtOperator jwtOperator;
public String login(UserLoginDTO loginDTO) {
// 1. 效验账号密码[直接使用明文 实际项目要使用加密 目前比较流行的加密算法有MD5/BCript/SHA1]是否匹配
// 让spring data jpa
return this.userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword())
.map(user -> {
// 2. 如果匹配,则颁发token,
HashMap<String, Object> userInfo = new HashMap<String, Object>();
userInfo.put("userId", user.getId());
userInfo.put("username", user.getUsername());
return jwtOperator.generateToken(userInfo);
}).orElseThrow(() -> new IllegalArgumentException("账户密码不匹配"));
}
启动用户微服务
2020-02-29 21:29:02.886 INFO 10936 --- [ restartedMain] o.s.b.devtools.restart.ChangeableUrls : The Class-Path manifest attribute in D:\maven\repo\org\glassfish\jaxb\jaxb-runtime\2.3.2\jaxb-runtime-2.3.2.jar referenced one or more files that do not exist: file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/jakarta.xml.bind-api-2.3.2.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/txw2-2.3.2.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/istack-commons-runtime-3.0.8.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/stax-ex-1.8.1.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/FastInfoset-1.2.16.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/jakarta.activation-api-1.2.1.jar
2020-02-29 21:29:02.889 INFO 10936 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2020-02-29 21:29:03.416 INFO 10936 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.retry.annotation.RetryConfiguration' of type [org.springframework.retry.annotation.RetryConfiguration$$EnhancerBySpringCGLIB$$8f150ae3] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.2.RELEASE)
2020-02-29 21:29:05.292 INFO 10936 --- [ restartedMain] com.cloud.msuser.MsUserApplication : The following profiles are active: dev
2020-02-29 21:29:06.775 INFO 10936 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2020-02-29 21:29:06.861 INFO 10936 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 76ms. Found 2 JPA repository interfaces.
2020-02-29 21:29:07.058 WARN 10936 --- [ restartedMain] o.s.boot.actuate.endpoint.EndpointId : Endpoint ID 'service-registry' contains invalid characters, please migrate to a valid format.
2020-02-29 21:29:07.446 INFO 10936 --- [ restartedMain] o.s.cloud.context.scope.GenericScope : BeanFactory id=66fe5b88-5b85-38ce-88d7-3ba7e1992f8b
2020-02-29 21:29:07.581 INFO 10936 --- [ restartedMain] faultConfiguringBeanFactoryPostProcessor : No bean named 'errorChannel' has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created.
2020-02-29 21:29:07.592 INFO 10936 --- [ restartedMain] faultConfiguringBeanFactoryPostProcessor : No bean named 'taskScheduler' has been explicitly defined. Therefore, a default ThreadPoolTaskScheduler will be created.
2020-02-29 21:29:07.599 INFO 10936 --- [ restartedMain] faultConfiguringBeanFactoryPostProcessor : No bean named 'integrationHeaderChannelRegistry' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created.
2020-02-29 21:29:07.671 INFO 10936 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.retry.annotation.RetryConfiguration' of type [org.springframework.retry.annotation.RetryConfiguration$$EnhancerBySpringCGLIB$$8f150ae3] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 21:29:07.704 INFO 10936 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 21:29:07.755 INFO 10936 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'integrationChannelResolver' of type [org.springframework.integration.support.channel.BeanFactoryChannelResolver] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 21:29:07.760 INFO 10936 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'integrationDisposableAutoCreatedBeans' of type [org.springframework.integration.config.annotation.Disposables] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 21:29:07.791 INFO 10936 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.integration.config.IntegrationManagementConfiguration' of type [org.springframework.integration.config.IntegrationManagementConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 21:29:07.798 INFO 10936 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration$IntegrationJmxConfiguration' of type [org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration$IntegrationJmxConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 21:29:07.811 INFO 10936 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration' of type [org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 21:29:07.818 INFO 10936 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'mbeanServer' of type [com.sun.jmx.mbeanserver.JmxMBeanServer] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 21:29:08.503 INFO 10936 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http)
2020-02-29 21:29:08.516 INFO 10936 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-02-29 21:29:08.516 INFO 10936 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.29]
2020-02-29 21:29:08.679 INFO 10936 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-02-29 21:29:08.679 INFO 10936 --- [ restartedMain] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 3352 ms
2020-02-29 21:29:08.826 WARN 10936 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : No URLs will be polled as dynamic configuration sources.
2020-02-29 21:29:08.827 INFO 10936 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : To enable URLs as dynamic configuration sources, define System property archaius.configurationSource.additionalUrls or make config.properties available on classpath.
2020-02-29 21:29:08.876 INFO 10936 --- [ restartedMain] c.netflix.config.DynamicPropertyFactory : DynamicPropertyFactory is initialized with configuration sources: com.netflix.config.ConcurrentCompositeConfiguration@3819297b
2020-02-29 21:29:10.214 INFO 10936 --- [ restartedMain] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
2020-02-29 21:29:10.616 INFO 10936 --- [ restartedMain] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2020-02-29 21:29:10.704 INFO 10936 --- [ restartedMain] org.hibernate.Version : HHH000412: Hibernate Core {5.4.9.Final}
2020-02-29 21:29:10.885 INFO 10936 --- [ restartedMain] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
2020-02-29 21:29:11.022 INFO 10936 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2020-02-29 21:29:11.183 INFO 10936 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2020-02-29 21:29:11.206 INFO 10936 --- [ restartedMain] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL57Dialect
2020-02-29 21:29:11.990 INFO 10936 --- [ restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-02-29 21:29:11.999 INFO 10936 --- [ restartedMain] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-02-29 21:29:12.913 WARN 10936 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : No URLs will be polled as dynamic configuration sources.
2020-02-29 21:29:12.913 INFO 10936 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : To enable URLs as dynamic configuration sources, define System property archaius.configurationSource.additionalUrls or make config.properties available on classpath.
2020-02-29 21:29:13.010 WARN 10936 --- [ restartedMain] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2020-02-29 21:29:13.145 INFO 10936 --- [ restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-02-29 21:29:13.867 INFO 10936 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2020-02-29 21:29:14.907 WARN 10936 --- [ restartedMain] ockingLoadBalancerClientRibbonWarnLogger : You already have RibbonLoadBalancerClient on your classpath. It will be used by default. As Spring Cloud Ribbon is in maintenance mode. We recommend switching to BlockingLoadBalancerClient instead. In order to use it, set the value of `spring.cloud.loadbalancer.ribbon.enabled` to `false` or remove spring-cloud-starter-netflix-ribbon from your project.
2020-02-29 21:29:14.967 INFO 10936 --- [ restartedMain] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'configWatchTaskScheduler'
2020-02-29 21:29:14.973 INFO 10936 --- [ restartedMain] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'catalogWatchTaskScheduler'
2020-02-29 21:29:15.002 INFO 10936 --- [ restartedMain] o.s.b.a.e.web.EndpointLinksResolver : Exposing 21 endpoint(s) beneath base path '/actuator'
2020-02-29 21:29:15.159 INFO 10936 --- [ restartedMain] o.s.c.s.m.DirectWithAttributesChannel : Channel 'ms-user-1.input' has 1 subscriber(s).
2020-02-29 21:29:15.324 INFO 10936 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel input
2020-02-29 21:29:15.457 INFO 10936 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel nullChannel
2020-02-29 21:29:15.485 INFO 10936 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel errorChannel
2020-02-29 21:29:15.581 INFO 10936 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageHandler org.springframework.cloud.stream.binding.StreamListenerMessageHandler@4e050db5
2020-02-29 21:29:15.691 INFO 10936 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageHandler errorLogger
2020-02-29 21:29:15.773 INFO 10936 --- [ restartedMain] o.s.i.endpoint.EventDrivenConsumer : Adding {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel
2020-02-29 21:29:15.774 INFO 10936 --- [ restartedMain] o.s.i.channel.PublishSubscribeChannel : Channel 'ms-user-1.errorChannel' has 1 subscriber(s).
2020-02-29 21:29:15.774 INFO 10936 --- [ restartedMain] o.s.i.endpoint.EventDrivenConsumer : started bean '_org.springframework.integration.errorLogger'
2020-02-29 21:29:16.662 INFO 10936 --- [ restartedMain] c.s.b.r.p.RabbitExchangeQueueProvisioner : declaring queue for inbound: lesson-buy.g1, bound to: lesson-buy
2020-02-29 21:29:16.668 INFO 10936 --- [ restartedMain] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [192.168.99.100:5672]
2020-02-29 21:29:16.731 INFO 10936 --- [ restartedMain] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#28d113c9:0/SimpleConnection@6b129f9a [delegate=amqp://admin@192.168.99.100:5672/, localPort= 55315]
2020-02-29 21:29:16.809 INFO 10936 --- [ restartedMain] o.s.c.stream.binder.BinderErrorChannel : Channel 'lesson-buy.g1.errors' has 1 subscriber(s).
2020-02-29 21:29:16.810 INFO 10936 --- [ restartedMain] o.s.c.stream.binder.BinderErrorChannel : Channel 'lesson-buy.g1.errors' has 2 subscriber(s).
2020-02-29 21:29:16.837 INFO 10936 --- [ restartedMain] o.s.i.a.i.AmqpInboundChannelAdapter : started bean 'inbound.lesson-buy.g1'
2020-02-29 21:29:16.870 INFO 10936 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel lesson-buy.g1.errors
2020-02-29 21:29:17.019 INFO 10936 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2020-02-29 21:29:17.039 INFO 10936 --- [ restartedMain] o.s.c.c.s.ConsulServiceRegistry : Registering service with consul: NewService{id='ms-user-8081', name='ms-user', tags=[a=b, c=d, JIFANG=NJ, secure=false], address='192.168.0.105', meta=null, port=8081, enableTagOverride=null, check=Check{script='null', interval='10s', ttl='null', http='http://192.168.0.105:8081/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null'}, checks=null}
2020-02-29 21:29:17.933 INFO 10936 --- [ restartedMain] com.cloud.msuser.MsUserApplication : Started MsUserApplication in 16.741 seconds (JVM running for 18.002)
2020-02-29 21:29:20.673 INFO 10936 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-02-29 21:29:20.674 INFO 10936 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-02-29 21:29:20.686 INFO 10936 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 12 ms
当前数据库数据有:
id | username | password | money | role | reg_time |
---|---|---|---|---|---|
1 | itmuch | 1111 | 3 | user | 2/15/2020 14:37:20 |
2 | jack | 1234 | 5 | user | 2/29/2020 21:27:59 |
更换密码
登录状态效验
在userController中通过findById查询用户,必须首先登录过才可以。
* 1. 如果用户没有登录,那么返回http 401
* 2. 如果已经登录了,那么正常访问
要实现上面的业务,有多种方式:
- Servlet过滤器:实现javax.servlet.Filter接口
- 基于Spring MVC拦截器:实现org.springframework.web.servlet.HandlerInterceptor接口
- Spring AOP 通过注解实现
本次采用第三种方案,因为基于注解比较干净,另外aop是一个比较重要的知识点。
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建一个注解类
package com.cloud.msuser.auth;
public @interface Login {
}
创建一个异常类
package com.cloud.msuser.auth;
public class SecurityException extends RuntimeException {
private static final long serialVersionUID = 1080896099168520967L;
public SecurityException() {
super();
}
public SecurityException(String message) {
super(message);
}
}
创建一个切面
package com.cloud.msuser.auth;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.cloud.msuser.jwt.JwtOperator;
import io.jsonwebtoken.Claims;
@Component
@Aspect
public class AuthAspect {
@Autowired
private JwtOperator jwtOperator;
@Around("@annotation(com.cloud.msuser.auth.Login)")
public Object checkLogin(ProceedingJoinPoint point) throws Throwable {
// 1.获取http请求中的header(token)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 此处约定一下 头信息中包含Authorization
String token = (String) request.getHeader("Authorization");
if(StringUtils.isEmpty(token)) {
throw new SecurityException("Token没有传!!");
}
// 2.校验token是否合法,如果合法就认为用户已登录,如果不合法,就返回401
Boolean isValid = this.jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("Token非法!!");
}
Claims userInfo = this.jwtOperator.getClaimsFromToken(token);
request.setAttribute("userId", userInfo.get("userId"));
request.setAttribute("username", userInfo.get("username"));
return point.proceed();
}
}
在UserController的findById方法上添加注解
/**
* 1. 如果用户没有登录,那么返回http 401
* 2. 如果已经登录了,那么正常访问
* http://localhost:8081/users/1
* @param id
* @return
*/
@Login
@GetMapping("/users/{id}")
public User findById(@PathVariable Integer id) {
return this.userService.findById(id);
}
再次启动用户微服务
2020-02-29 22:56:17.400 INFO 13000 --- [ restartedMain] o.s.b.devtools.restart.ChangeableUrls : The Class-Path manifest attribute in D:\maven\repo\org\glassfish\jaxb\jaxb-runtime\2.3.2\jaxb-runtime-2.3.2.jar referenced one or more files that do not exist: file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/jakarta.xml.bind-api-2.3.2.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/txw2-2.3.2.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/istack-commons-runtime-3.0.8.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/stax-ex-1.8.1.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/FastInfoset-1.2.16.jar,file:/D:/maven/repo/org/glassfish/jaxb/jaxb-runtime/2.3.2/jakarta.activation-api-1.2.1.jar
2020-02-29 22:56:17.403 INFO 13000 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2020-02-29 22:56:17.949 INFO 13000 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.retry.annotation.RetryConfiguration' of type [org.springframework.retry.annotation.RetryConfiguration$$EnhancerBySpringCGLIB$$e5d01e41] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.2.RELEASE)
2020-02-29 22:56:19.609 INFO 13000 --- [ restartedMain] com.cloud.msuser.MsUserApplication : The following profiles are active: dev
2020-02-29 22:56:21.091 INFO 13000 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2020-02-29 22:56:21.179 INFO 13000 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 78ms. Found 2 JPA repository interfaces.
2020-02-29 22:56:21.379 WARN 13000 --- [ restartedMain] o.s.boot.actuate.endpoint.EndpointId : Endpoint ID 'service-registry' contains invalid characters, please migrate to a valid format.
2020-02-29 22:56:21.722 INFO 13000 --- [ restartedMain] o.s.cloud.context.scope.GenericScope : BeanFactory id=b2f66900-62c0-3838-b184-5633f95778ea
2020-02-29 22:56:21.833 INFO 13000 --- [ restartedMain] faultConfiguringBeanFactoryPostProcessor : No bean named 'errorChannel' has been explicitly defined. Therefore, a default PublishSubscribeChannel will be created.
2020-02-29 22:56:21.840 INFO 13000 --- [ restartedMain] faultConfiguringBeanFactoryPostProcessor : No bean named 'taskScheduler' has been explicitly defined. Therefore, a default ThreadPoolTaskScheduler will be created.
2020-02-29 22:56:21.846 INFO 13000 --- [ restartedMain] faultConfiguringBeanFactoryPostProcessor : No bean named 'integrationHeaderChannelRegistry' has been explicitly defined. Therefore, a default DefaultHeaderChannelRegistry will be created.
2020-02-29 22:56:21.905 INFO 13000 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.retry.annotation.RetryConfiguration' of type [org.springframework.retry.annotation.RetryConfiguration$$EnhancerBySpringCGLIB$$e5d01e41] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 22:56:21.954 INFO 13000 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 22:56:22.137 INFO 13000 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'integrationChannelResolver' of type [org.springframework.integration.support.channel.BeanFactoryChannelResolver] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 22:56:22.142 INFO 13000 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'integrationDisposableAutoCreatedBeans' of type [org.springframework.integration.config.annotation.Disposables] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 22:56:22.205 INFO 13000 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.integration.config.IntegrationManagementConfiguration' of type [org.springframework.integration.config.IntegrationManagementConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 22:56:22.235 INFO 13000 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration$IntegrationJmxConfiguration' of type [org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration$IntegrationJmxConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 22:56:22.250 INFO 13000 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration' of type [org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 22:56:22.259 INFO 13000 --- [ restartedMain] trationDelegate$BeanPostProcessorChecker : Bean 'mbeanServer' of type [com.sun.jmx.mbeanserver.JmxMBeanServer] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-02-29 22:56:23.296 INFO 13000 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http)
2020-02-29 22:56:23.306 INFO 13000 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-02-29 22:56:23.306 INFO 13000 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.29]
2020-02-29 22:56:23.470 INFO 13000 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-02-29 22:56:23.470 INFO 13000 --- [ restartedMain] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 3832 ms
2020-02-29 22:56:23.664 WARN 13000 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : No URLs will be polled as dynamic configuration sources.
2020-02-29 22:56:23.664 INFO 13000 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : To enable URLs as dynamic configuration sources, define System property archaius.configurationSource.additionalUrls or make config.properties available on classpath.
2020-02-29 22:56:23.682 INFO 13000 --- [ restartedMain] c.netflix.config.DynamicPropertyFactory : DynamicPropertyFactory is initialized with configuration sources: com.netflix.config.ConcurrentCompositeConfiguration@57c47da2
2020-02-29 22:56:25.582 INFO 13000 --- [ restartedMain] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
2020-02-29 22:56:26.370 INFO 13000 --- [ restartedMain] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2020-02-29 22:56:26.448 INFO 13000 --- [ restartedMain] org.hibernate.Version : HHH000412: Hibernate Core {5.4.9.Final}
2020-02-29 22:56:26.585 INFO 13000 --- [ restartedMain] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
2020-02-29 22:56:26.707 INFO 13000 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2020-02-29 22:56:26.850 INFO 13000 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2020-02-29 22:56:26.870 INFO 13000 --- [ restartedMain] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL57Dialect
2020-02-29 22:56:27.880 INFO 13000 --- [ restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-02-29 22:56:27.888 INFO 13000 --- [ restartedMain] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-02-29 22:56:28.561 WARN 13000 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : No URLs will be polled as dynamic configuration sources.
2020-02-29 22:56:28.562 INFO 13000 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : To enable URLs as dynamic configuration sources, define System property archaius.configurationSource.additionalUrls or make config.properties available on classpath.
2020-02-29 22:56:28.692 WARN 13000 --- [ restartedMain] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2020-02-29 22:56:28.896 INFO 13000 --- [ restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-02-29 22:56:30.026 INFO 13000 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2020-02-29 22:56:31.113 WARN 13000 --- [ restartedMain] ockingLoadBalancerClientRibbonWarnLogger : You already have RibbonLoadBalancerClient on your classpath. It will be used by default. As Spring Cloud Ribbon is in maintenance mode. We recommend switching to BlockingLoadBalancerClient instead. In order to use it, set the value of `spring.cloud.loadbalancer.ribbon.enabled` to `false` or remove spring-cloud-starter-netflix-ribbon from your project.
2020-02-29 22:56:31.230 INFO 13000 --- [ restartedMain] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'configWatchTaskScheduler'
2020-02-29 22:56:31.241 INFO 13000 --- [ restartedMain] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'catalogWatchTaskScheduler'
2020-02-29 22:56:31.408 INFO 13000 --- [ restartedMain] o.s.b.a.e.web.EndpointLinksResolver : Exposing 21 endpoint(s) beneath base path '/actuator'
2020-02-29 22:56:31.661 INFO 13000 --- [ restartedMain] o.s.c.s.m.DirectWithAttributesChannel : Channel 'ms-user-1.input' has 1 subscriber(s).
2020-02-29 22:56:31.824 INFO 13000 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel errorChannel
2020-02-29 22:56:31.929 INFO 13000 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel input
2020-02-29 22:56:31.979 INFO 13000 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel nullChannel
2020-02-29 22:56:32.008 INFO 13000 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageHandler org.springframework.cloud.stream.binding.StreamListenerMessageHandler@72f958d1
2020-02-29 22:56:32.095 INFO 13000 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageHandler errorLogger
2020-02-29 22:56:32.171 INFO 13000 --- [ restartedMain] o.s.i.endpoint.EventDrivenConsumer : Adding {logging-channel-adapter:_org.springframework.integration.errorLogger} as a subscriber to the 'errorChannel' channel
2020-02-29 22:56:32.171 INFO 13000 --- [ restartedMain] o.s.i.channel.PublishSubscribeChannel : Channel 'ms-user-1.errorChannel' has 1 subscriber(s).
2020-02-29 22:56:32.172 INFO 13000 --- [ restartedMain] o.s.i.endpoint.EventDrivenConsumer : started bean '_org.springframework.integration.errorLogger'
2020-02-29 22:56:32.892 INFO 13000 --- [ restartedMain] c.s.b.r.p.RabbitExchangeQueueProvisioner : declaring queue for inbound: lesson-buy.g1, bound to: lesson-buy
2020-02-29 22:56:32.895 INFO 13000 --- [ restartedMain] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [192.168.99.100:5672]
2020-02-29 22:56:32.942 INFO 13000 --- [ restartedMain] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#2292367c:0/SimpleConnection@3967bc8d [delegate=amqp://admin@192.168.99.100:5672/, localPort= 59128]
2020-02-29 22:56:33.008 INFO 13000 --- [ restartedMain] o.s.c.stream.binder.BinderErrorChannel : Channel 'lesson-buy.g1.errors' has 1 subscriber(s).
2020-02-29 22:56:33.008 INFO 13000 --- [ restartedMain] o.s.c.stream.binder.BinderErrorChannel : Channel 'lesson-buy.g1.errors' has 2 subscriber(s).
2020-02-29 22:56:33.034 INFO 13000 --- [ restartedMain] o.s.i.a.i.AmqpInboundChannelAdapter : started bean 'inbound.lesson-buy.g1'
2020-02-29 22:56:33.061 INFO 13000 --- [ restartedMain] o.s.i.monitor.IntegrationMBeanExporter : Registering MessageChannel lesson-buy.g1.errors
2020-02-29 22:56:33.202 INFO 13000 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2020-02-29 22:56:33.220 INFO 13000 --- [ restartedMain] o.s.c.c.s.ConsulServiceRegistry : Registering service with consul: NewService{id='ms-user-8081', name='ms-user', tags=[a=b, c=d, JIFANG=NJ, secure=false], address='192.168.0.105', meta=null, port=8081, enableTagOverride=null, check=Check{script='null', interval='10s', ttl='null', http='http://192.168.0.105:8081/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null'}, checks=null}
2020-02-29 22:56:33.754 INFO 13000 --- [ restartedMain] com.cloud.msuser.MsUserApplication : Started MsUserApplication in 17.589 seconds (JVM running for 18.892)
2020-02-29 22:56:39.852 INFO 13000 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-02-29 22:56:39.853 INFO 13000 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-02-29 22:56:39.875 INFO 13000 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 22 ms
通过post请求获取一个token:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjIsInVzZXJuYW1lIjoiamFjayIsImlhdCI6MTU4Mjk4ODMyNywiZXhwIjoxNTg0MTk3OTI3fQ.UisHgud-IvPOKXDG8R15h84SeFMuCflbwTRrClpDOUM
但是上面请求失败返回的不是401,而是500,通过全局异常处理器进行处理:
package com.cloud.msuser.auth;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(SecurityException.class)
public ResponseEntity<Response> result(SecurityException exception) {
Response response = new Response(exception.getMessage(), 401);
return new ResponseEntity<Response>(response, HttpStatus.UNAUTHORIZED);
}
}
class Response {
private String message;
private Integer code;
public Response() {
}
public Response(String message, Integer code) {
this.message = message;
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
但是还存在如下问题 如果传递错了token时 返回的是500 而且异常信息为 Token invalided.
这是因为切面里面在验证token有效性时抛出了异常,修改代码,对切面代码进行整体try…catch…处理
package com.cloud.msuser.auth;
public class SecurityException extends RuntimeException {
private static final long serialVersionUID = 1080896099168520967L;
public SecurityException() {
super();
}
public SecurityException(String message) {
super(message);
}
public SecurityException(String message, Throwable cause) {
super(message, cause);
}
public SecurityException(Throwable cause) {
super(cause);
}
}
package com.cloud.msuser.auth;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.cloud.msuser.jwt.JwtOperator;
import io.jsonwebtoken.Claims;
@Component
@Aspect
public class AuthAspect {
@Autowired
private JwtOperator jwtOperator;
@Around("@annotation(com.cloud.msuser.auth.Login)")
public Object checkLogin(ProceedingJoinPoint point) {
try {
// 1.获取http请求中的header(token)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 此处约定一下 头信息中包含Authorization
String token = (String) request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new SecurityException("Token没有传!!");
}
// 2.校验token是否合法,如果合法就认为用户已登录,如果不合法,就返回401
Boolean isValid = this.jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("Token非法!!");
}
Claims userInfo = this.jwtOperator.getClaimsFromToken(token);
request.setAttribute("userId", userInfo.get("userId"));
request.setAttribute("username", userInfo.get("username"));
return point.proceed();
} catch (Throwable e) {
throw new SecurityException(e);
}
}
}
课程微服务实现登录状态效验
跟用户微服务一样引入依赖包、注解以及切面
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
package com.cloud.msclass.auth;
public @interface Login {
}
package com.cloud.msclass.auth;
public class SecurityException extends RuntimeException {
private static final long serialVersionUID = 1080896099168520967L;
public SecurityException() {
super();
}
public SecurityException(String message) {
super(message);
}
public SecurityException(String message, Throwable cause) {
super(message, cause);
}
public SecurityException(Throwable cause) {
super(cause);
}
}
package com.cloud.msclass.auth;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.cloud.msclass.jwt.JwtOperator;
import io.jsonwebtoken.Claims;
@Component
@Aspect
public class AuthAspect {
@Autowired
private JwtOperator jwtOperator;
@Around("@annotation(com.cloud.msuser.auth.Login)")
public Object checkLogin(ProceedingJoinPoint point) {
try {
// 1.获取http请求中的header(token)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 此处约定一下 头信息中包含Authorization
String token = (String) request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new SecurityException("Token没有传!!");
}
// 2.校验token是否合法,如果合法就认为用户已登录,如果不合法,就返回401
Boolean isValid = this.jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("Token非法!!");
}
Claims userInfo = this.jwtOperator.getClaimsFromToken(token);
request.setAttribute("userId", userInfo.get("userId"));
request.setAttribute("username", userInfo.get("username"));
return point.proceed();
} catch (Throwable e) {
throw new SecurityException(e);
}
}
}
package com.cloud.msclass.auth;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(SecurityException.class)
public ResponseEntity<Response> result(SecurityException exception) {
Response response = new Response(exception.getMessage(), 401);
return new ResponseEntity<Response>(response, HttpStatus.UNAUTHORIZED);
}
}
class Response {
private String message;
private Integer code;
public Response() {
}
public Response(String message, Integer code) {
this.message = message;
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
首先执行用户登录,然后再执行课程微服务的买课程服务,如下:、
首先删掉lesson_user表中的数据:
再次执行操作
http://localhost:8010/lesssons/buy/1
{
"id": null,
"title": null,
"cover": null,
"price": null,
"description": null,
"createTime": null,
"videoUrl": null
}
查看后台日志:
2020-03-01 12:50:30.846 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] ---> GET http://ms-user/users/2 HTTP/1.1
2020-03-01 12:50:30.846 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] ---> END HTTP (0-byte body)
2020-03-01 12:50:31.020 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] <--- HTTP/1.1 401 (173ms)
2020-03-01 12:50:31.021 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] connection: keep-alive
2020-03-01 12:50:31.021 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] content-type: application/json
2020-03-01 12:50:31.021 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] date: Sun, 01 Mar 2020 04:50:31 GMT
2020-03-01 12:50:31.021 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] keep-alive: timeout=60
2020-03-01 12:50:31.021 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] transfer-encoding: chunked
2020-03-01 12:50:31.021 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById]
2020-03-01 12:50:31.022 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] {"message":"com.cloud.msuser.auth.SecurityException: Token没有传!!","code":401}
2020-03-01 12:50:31.022 DEBUG 13556 --- [nio-8010-exec-3] c.cloud.msclass.feign.MsUserFeignClient : [MsUserFeignClient#findUserById] <--- END HTTP (86-byte body)
2020-03-01 12:50:31.046 ERROR 13556 --- [nio-8010-exec-3] c.c.msclass.controller.LessonController : 发生fallback
com.cloud.msclass.auth.SecurityException: feign.FeignException$Unauthorized: status 401 reading MsUserFeignClient#findUserById(Integer)
由此可见 是由于feign请求导致出现了异常 然后触发了fallback,但是我们希望的是通过SecurityException,再触发401
因此需要修改切面与服务容错的先后顺序
// 添加Order注解
@Order(1)
@Component
@Aspect
public class AuthAspect {
@Autowired
private JwtOperator jwtOperator;
@Around("@annotation(com.cloud.msclass.auth.Login)")
public Object checkLogin(ProceedingJoinPoint point) {
try {
// 1.获取http请求中的header(token)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 此处约定一下 头信息中包含Authorization
String token = (String) request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new SecurityException("Token没有传!!");
}
// 2.校验token是否合法,如果合法就认为用户已登录,如果不合法,就返回401
Boolean isValid = this.jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("Token非法!!");
}
Claims userInfo = this.jwtOperator.getClaimsFromToken(token);
request.setAttribute("userId", userInfo.get("userId"));
request.setAttribute("username", userInfo.get("username"));
return point.proceed();
} catch (Throwable e) {
throw new SecurityException(e);
}
}
}
查看日志
2020-03-01 12:50:31.002 WARN 12596 --- [nio-8081-exec-6] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [com.cloud.msuser.auth.SecurityException: com.cloud.msuser.auth.SecurityException: Token没有传!!]
2020-03-01 12:57:14.696 WARN 12596 --- [nio-8081-exec-9] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [com.cloud.msuser.auth.SecurityException: com.cloud.msuser.auth.SecurityException: Token没有传!!]
{
"id": null,
"title": null,
"cover": null,
"price": null,
"description": null,
"createTime": null,
"videoUrl": null
}
如果此时token错误的话,则返回
2020-03-01 13:05:37.531 WARN 10616 --- [nio-8010-exec-9] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [com.cloud.msclass.auth.SecurityException: java.lang.IllegalArgumentException: Token invalided.]
{
"message": "java.lang.IllegalArgumentException: Token invalided.",
"code": 401
}
下面需要解决调用feign接口调用的错误问题
Feign实现Token传递
修改feign接口以及相应的代码(添加token参数)
@FeignClient(name = "ms-user") // 底层使用ribbon去请求
public interface MsUserFeignClient {
@GetMapping("/users/{userId}")
UserDTO findUserById(@PathVariable("userId") Integer userId,@RequestHeader("Authorization") String token);
}
@Service
@Transactional(rollbackFor = Exception.class)
public class LessonService {
@Autowired
private LessonRepository lessonRepository;
@Autowired
private LessonUserRepository lessonUserRepository;
@Autowired
private Source source;
@Autowired
private MsUserFeignClient msUserFeignClient;
public Lesson buyById(Integer id, Integer userId,String token) {
// 1. 根据id查询lesson
Lesson lesson = this.lessonRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("该课程不存在"));
// 2. 根据lesson.id查询user_lesson,那么直接返回lesson
LessonUser lessonUser = this.lessonUserRepository.findByLessonId(id);
if (lessonUser != null) {
return lesson;
}
// 3. 如果user_lesson==null && 用户的余额 > lesson.price 则购买成功
UserDTO userDTO = this.msUserFeignClient.findUserById(userId,token);
BigDecimal money = userDTO.getMoney().subtract(lesson.getPrice());
if (money.doubleValue() < 0) {
throw new IllegalArgumentException("余额不足");
}
// 购买逻辑 ... 1. 发送消息给用户微服务 让它扣减金额
String description = String.format("%s购买了id为%s的课程", userId, id);
this.source.output().send(
MessageBuilder.withPayload(new UserMoneyDTO(userId, lesson.getPrice(), "购买课程", description)).build());
// 2.向lesson_user表插入数据
LessonUser lu = new LessonUser();
lu.setLessonId(id);
lu.setUserId(userId);
this.lessonUserRepository.save(lu);
return lesson;
}
}
@RestController
@RequestMapping("lesssons")
public class LessonController {
private static final Logger logger = LoggerFactory.getLogger(LessonController.class);
@Autowired
private LessonService lessonService;
/**
* http://localhost:8010/lesssons/buy/1 购买指定id的课程
*
* @param id
*/
@GetMapping("/buy/{id}")
@RateLimiter(name = "buyById", fallbackMethod = "buyByIdFallBack")
@Login
public Lesson buyById(@PathVariable Integer id, HttpServletRequest request,
@RequestHeader("Authorization") String token) {
Integer userId = (Integer) request.getAttribute("userId");
return this.lessonService.buyById(id, userId, token);
}
// 必须与原方法有相同的返回值和参数(侯曼带一个Throwable参数)
public Lesson buyByIdFallBack(@PathVariable Integer id, HttpServletRequest request, String token,
Throwable throwable) {
// 表示从本地缓存获取
logger.error("发生fallback", throwable);
return new Lesson();
}
}
执行http://localhost:8010/lesssons/buy/1操作(header附带token信息):
{
"id": 1,
"title": "SpringCloud视频教程",
"cover": "xxx",
"price": 5,
"description": "SpringCloud视频教程",
"createTime": "2020-02-15T15:50:35.000+0000",
"videoUrl": "https://ke.qq.com/classroom/index.html"
}
但是在实际项目中,如此修改,如果接口非常多,则非常麻烦,因此采用第二种方案:
RequestInterceptor
将上面所有加了token的代码进行还原 并删除数据库中lesson_user的数据
package com.cloud.msclass.interceptor;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class MyHeaderRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String token = (String) request.getHeader("Authorization");
template.header("Authorization", token);
}
}
添加feign配置
feign:
client:
config:
default:
logger-level: full
request-interceptors:
- com.cloud.msclass.interceptor.MyHeaderRequestInterceptor
再次测试,OK
下面在controller方法上添加服务容错的注解(仓壁模式):
@GetMapping("/buy/{id}")
@RateLimiter(name = "buyById", fallbackMethod = "buyByIdFallBack")
@Bulkhead(name = "buyById", fallbackMethod = "buyByIdFallBack",type = Type.THREADPOOL)
@Login
public Lesson buyById(@PathVariable Integer id, HttpServletRequest request) {
Integer userId = (Integer) request.getAttribute("userId");
return this.lessonService.buyById(id, userId);
}
程序会报错:
2020-03-01 13:40:09.992 ERROR 9992 --- [nio-8010-exec-3] c.c.msclass.controller.LessonController : 发生fallback
java.lang.IllegalStateException: ThreadPool bulkhead is only applicable for completable futures
因为基于线程池的Bulkhead无法传递ThreadLocal.因为MyHeaderRequestInterceptor中引用的RequestContextHolder是通过ThreadLocal来保存和传递线程的。如果应用使用了ThreadLocal,不要去使用基于线程池的BulkHead.
删掉上面增加的Bulkhead注解。
RestTemplate传递Token
通过exchange
@Autowired
private RestTemplate restTemplate;
@GetMapping("/test-token-relay")
public ResponseEntity<UserDTO> testTokenRelay(@RequestHeader("Authorization") String token) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
return this.restTemplate.exchange("http://ms-user/users/{id}", HttpMethod.GET, new HttpEntity<String>(headers),
UserDTO.class, 2);
}
通过POSTMAN发起请求 http://localhost:8010/test-token-relay header中包含token信息 返回结果如下:
{
"id": 2,
"username": "jack",
"password": "1234",
"money": 145,
"role": "user",
"regTime": "2020-02-29T21:27:59.000+0000"
}
通过ClientHttpRequestInteceptor接口
package com.cloud.msclass.interceptor;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
public class TokenRelayRequestInteceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
HttpHeaders headers = request.getHeaders();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest servletRequest = attributes.getRequest();
// 此处约定一下 头信息中包含Authorization
String token = (String) servletRequest.getHeader("Authorization");
headers.add("Authorization", token);
// 保证请求继续执行...
return execution.execute(request, body);
}
}
修改RestTemplate类实例:
/**
* spring web提供的轻量级http client
*
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new TokenRelayRequestInteceptor()));
return restTemplate;
}
在TestController中添加测试方法:
/**
* http://localhost:8010/test-token-relay2
*
* @param token
* @return
*/
@GetMapping("/test-token-relay2")
public UserDTO testTokenRelay2() {
return this.restTemplate.getForObject("http://ms-user/users/{id}", UserDTO.class, 2);
}
授权
在login服务中添加role信息
public String login(UserLoginDTO loginDTO) {
// 1. 效验账号密码[直接使用明文 实际项目要使用加密 目前比较流行的加密算法有MD5/BCript/SHA1]是否匹配
// 让spring data jpa
return this.userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword())
.map(user -> {
// 2. 如果匹配,则颁发token,
HashMap<String, Object> userInfo = new HashMap<String, Object>();
userInfo.put("userId", user.getId());
userInfo.put("username", user.getUsername());
// 添加上role信息
userInfo.put("role", user.getRole());
return jwtOperator.generateToken(userInfo);
}).orElseThrow(() -> new IllegalArgumentException("账户密码不匹配"));
}
package com.cloud.msclass.auth;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/*
* Retention 指定注解的保留策略
* RUNTIME 注解会在字节码中存在,并且可以通过反射获取
* 元注解: 注解在注解类上的注解
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthz {
String hasRole();
}
在切面中添加验证角色的逻辑
package com.cloud.msclass.auth;
import java.lang.reflect.Method;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.cloud.msclass.jwt.JwtOperator;
import io.jsonwebtoken.Claims;
@Order(1)
@Component
@Aspect
public class AuthAspect {
@Autowired
private JwtOperator jwtOperator;
@Around("@annotation(com.cloud.msclass.auth.Login)")
public Object checkLogin(ProceedingJoinPoint point) {
try {
// 1.获取http请求中的header(token)
validateToken();
return point.proceed();
} catch (Throwable e) {
throw new SecurityException(e);
}
}
@Around("@annotation(com.cloud.msclass.auth.CheckAuthz)")
public Object checkAuth(ProceedingJoinPoint point) {
try {
// 1.获取http请求中的header(token)
HttpServletRequest request = validateToken();
// 2 判断角色是否OK
Object role = request.getAttribute("role");
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
CheckAuthz checkAuthz = method.getAnnotation(CheckAuthz.class);
String hasRole = checkAuthz.hasRole();
if (!Objects.equals(hasRole, role)) {
throw new SecurityException("当前用户不具有角色" + hasRole);
}
return point.proceed();
} catch (Throwable e) {
throw new SecurityException(e);
}
}
private HttpServletRequest validateToken() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 此处约定一下 头信息中包含Authorization
String token = (String) request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new SecurityException("Token没有传!!");
}
// 2.校验token是否合法,如果合法就认为用户已登录,如果不合法,就返回401
Boolean isValid = this.jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("Token非法!!");
}
Claims userInfo = this.jwtOperator.getClaimsFromToken(token);
request.setAttribute("userId", userInfo.get("userId"));
request.setAttribute("username", userInfo.get("username"));
request.setAttribute("role", userInfo.get("role"));
return request;
}
}
创建测试方法
/**
* 当且仅当当前用户是vip 才可以访问 http://localhost:8010/vip
*
* @param token
* @return
*/
@GetMapping("/vip")
@CheckAuthz(hasRole = "vip")
public UserDTO vip() {
return this.restTemplate.getForObject("http://ms-user/users/{id}", UserDTO.class, 2);
}
测试:
-- 保证数据库中存在如下数据
INSERT INTO `ms_user`.`user` (`id`, `username`, `password`, `money`, `role`, `reg_time`) VALUES ('2', 'jack', '1234', '145', 'user', '2020-02-29 21:27:59');
INSERT INTO `ms_user`.`user` (`id`, `username`, `password`, `money`, `role`, `reg_time`) VALUES ('3', 'king', '1234', '145', 'vip', '2020-02-29 21:27:59');
登录用户jack
并访问 http://localhost:8010/vip
返回如下
{
"message": "com.cloud.msclass.auth.SecurityException: 当前用户不具有角色vip",
"code": 401
}
登录用户king
并访问 http://localhost:8010/vip
返回如下
{
"id": 2,
"username": "jack",
"password": "1234",
"money": 145,
"role": "user",
"regTime": "2020-02-29T21:27:59.000+0000"
}
可以查询到数据库信息
总结
登录认证的四种方案
授权常用方案
基于AOP实现认证授权
Feign/RestTemplate如何传递Token