Oauth2.0 系列文章
代表实现:
示例代码:
Spring Cloud Security示例代码1
Spring Cloud Security示例代码2
JBoss Keycloak示例代码
RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般是多对多的关系。(如下图)
权限
系统的所有权限信息。权限具有上下级关系,是一个树状的结构。如:
用户
系统的具体操作者,可以归属于一个或多个角色,它与角色的关系是多对多的关系
角色
为了对许多拥有相似权限的用户进行分类管理,定义了角色的概念,例如系统管理员、管理员、用户、访客等角色。角色具有上下级关系,可以形成树状视图,父级角色的权限是自身及它的所有子角色的权限的综合。父级角色的用户、父级角色的组同理可推。
CREATE TABLE `tb_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',
`name` varchar(64) NOT NULL COMMENT '权限名称',
`enname` varchar(64) NOT NULL COMMENT '权限英文名称',
`url` varchar(255) NOT NULL COMMENT '授权路径',
`description` varchar(200) DEFAULT NULL COMMENT '备注',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='权限表';
CREATE TABLE `tb_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
`name` varchar(64) NOT NULL COMMENT '角色名称',
`enname` varchar(64) NOT NULL COMMENT '角色英文名称',
`description` varchar(200) DEFAULT NULL COMMENT '备注',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='角色表';
CREATE TABLE `tb_role_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL COMMENT '角色 ID',
`permission_id` bigint(20) NOT NULL COMMENT '权限 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='角色权限表';
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(64) NOT NULL COMMENT '密码,加密存储',
`phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
`email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`) USING BTREE,
UNIQUE KEY `phone` (`phone`) USING BTREE,
UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='用户表';
CREATE TABLE `tb_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户 ID',
`role_id` bigint(20) NOT NULL COMMENT '角色 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT='用户角色表';
JWT全称Json web token
,是一个开放标准(RFC 7519),用来在各方之间安全地传输信息。JWT可被验证和信任,因为它是数字签名的。
jjwt GitHub
jjwt 自定义工具类参考
pom.xml
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.10.7version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.10.7version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.10.7version>
<scope>runtimescope>
dependency>
工具类
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
@Component
public class JwtOperator {
/**
* 秘钥
* - 默认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);
}
}
配置
jwt:
# 秘钥(各个微服务的秘钥一定要一致)
secret: aaaaaaaaaaassssssssscxzdfacdg
# 有效期,单位秒,默认2周
expire-time-in-second: 1209600
测试使用
@Autowired
private JwtOperator jwtOperator;
public static void main(String[] args) {
// 1. 初始化
JwtOperator jwtOperator = new JwtOperator();
jwtOperator.expirationTimeInSecond = 1209600L;
jwtOperator.secret = "aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt";
// 2.设置用户信息
HashMap<String, Object> objectObjectHashMap = Maps.newHashMap();
objectObjectHashMap.put("id", "1");
// 测试1: 生成token
String token = jwtOperator.generateToken(objectObjectHashMap);
// 会生成类似该字符串的内容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ
System.out.println(token);
// 将我改成上面生成的token!!!
String someToken = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ";
// 测试2: 如果能token合法且未过期,返回true
Boolean validateToken = jwtOperator.validateToken(someToken);
System.out.println(validateToken);
// 测试3: 获取用户信息
Claims claims = jwtOperator.getClaimsFromToken(someToken);
System.out.println(claims);
// 将我改成你生成的token的第一段(以.为边界)
String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";
// 测试4: 解密Header
byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
System.out.println(new String(header));
// 将我改成你生成的token的第二段(以.为边界)
String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk1NDEsImV4cCI6MTU2Njc5OTE0MX0";
// 测试5: 解密Payload
byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
System.out.println(new String(payload));
// 测试6: 这是一个被篡改的token,因此会报异常,说明JWT是安全的
jwtOperator.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk3MzIsImV4cCI6MTU2Njc5OTMzMn0.nDv25ex7XuTlmXgNzGX46LqMZItVFyNHQpmL9UQf-aUx");
}
前端部分代码
login(e) {
const self = this;
let userInfo = e.mp.detail.userInfo;
// 登录
wx.login({
success: (res) => {
request(
LOGIN_URL,
'POST', {
code: res.code,
wxNickname: userInfo.nickName,
avatarUrl: userInfo.avatarUrl
}
).then(res => {
console.log('登录成功...', res);
wx.setStorageSync('token', res.token);
wx.setStorageSync('user', res.user);
console.log('user...', res.user);
wx.showToast({
title: '登录成功!'
});
console.log('user...', res.user);
self.user = res.user;
}).catch(error => {
console.log('error', error);
reject(error)
});
}
});
},
后端
pom.xml
<dependency>
<groupId>com.github.binarywanggroupId>
<artifactId>weixin-java-miniappartifactId>
<version>3.5.0version>
dependency>
configuration
@Configuration
public class WxConfig {
@Bean
public WxMaConfig wxMaConfig() {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
//appid和secret可在微信公众平台查看
config.setAppid("xxxxxxxxxxxx");
config.setSecret("xxxxxxxxxxxxxxxxxxxxxxxxxx");
return config;
}
@Bean
public WxMaService wxMaService(WxMaConfig wxMaConfig) {
WxMaServiceImpl wxMaService = new WxMaServiceImpl();
wxMaService.setWxMaConfig(wxMaConfig);
return wxMaService;
}
}
Controller代码
@PostMapping("/login")
public LoginRespDTO login(@RequestBody UserLoginDTO loginDTO) throws WxErrorException {
//微信小程序服务端校验是否已经登录的结果
WxMaJscode2SessionResult result = wxMaService.getUserService().getSessionInfo(loginDTO.getCode());
//微信的openId,用户在微信的唯一标识
String openid = result.getOpenid();
/**
*看用户是否已经注册到数据库
*如果未注册,插入返回新user
*如果已经注册,返回user
*/
User user = this.userService.login(loginDTO, openid);
//颁发token
Map<String,Object> userInfo = new HashMap<>(3);
userInfo.put("id",user.getId());
userInfo.put("wxNickname",user.getWxNickname());
userInfo.put("role",user.getRoles());
String token = jwtOperator.generateToken(userInfo);
//日志
log.info("用户 {} 登录成功,生成的token = {},有效期为 {}",
user.getWxNickname(),token,jwtOperator.getExpirationTime());
//构建响应
return LoginRespDTO.builder().user(
UserRespDTO.builder()
.id(user.getId())
.avatarUrl(user.getAvatarUrl())
.bonus(user.getBonus())
.wxNickname(user.getWxNickname())
.build())
.token(JwtTokenRespDTO.builder()
.token(token)
.expirationTime(jwtOperator.getExpirationTime().getTime())
.build()
).build();
}
Service代码
public User login(UserLoginDTO loginDTO,String openId) {
User user = this.userMapper.selectOne(
User.builder().wxId(openId).build()
);
if (user == null) {
User userToSave = User.builder()
.wxId(openId)
.bonus(300)
.wxNickname(loginDTO.getWxNickname())
.avatarUrl(loginDTO.getAvatarUrl())
.roles("user")
.createTime(new Date())
.updateTime(new Date())
.build();
this.userMapper.insertSelective(
userToSave
);
return userToSave;
}
return user;
}
使用Spring AOP实现登录检查状态
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
新建注解
public @interface CheckLogin {
}
新建AOP切面,实现加有注解的地方需要进行token验证
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AuthAspect {
private final JwtOperator jwtOperator;
/**
* 检查登录状态
* @param point
* @return
*/
@Around("@annotation(com.banmingi.nodeapp.contentcenter.auth.CheckLogin)")
public Object checkLogin(ProceedingJoinPoint point) throws Throwable {
this.checkToken();
return point.proceed();
}
private void checkToken() {
try {
//1. 从header里面获取token
HttpServletRequest request = getHttpServletRequest();
String token = request.getHeader("X-Token");
//2. 校验token是否合法或在有效期内,如果不合法或已过期,直接抛异常;如果合法或未过期,放行
Boolean isValid = jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("token 不合法!");
}
//3. 如果校验成功,就将用户的信息设置到request的attribute里面
Claims claims = jwtOperator.getClaimsFromToken(token);
request.setAttribute("id",claims.get("id"));
request.setAttribute("wxNickname",claims.get("wxNickname"));
request.setAttribute("role",claims.get("role"));
} catch (Throwable throwable) {
throw new SecurityException("token 不合法!");
}
}
/**
* 获取request
* @return
*/
private HttpServletRequest getHttpServletRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes)requestAttributes;
return attributes.getRequest();
}
}
异常处理
public class SecurityException extends RuntimeException {
public SecurityException(String message) {
super(message);
}
public SecurityException(String message, Throwable cause) {
super(message, cause);
}
}
@RestControllerAdvice
@Slf4j
public class GlobalExceptionErrorHander {
@ExceptionHandler(SecurityException.class)
public ResponseEntity<ErrorBody> error(SecurityException e) {
log.warn("发生Security异常",e);
return new ResponseEntity<ErrorBody>(
ErrorBody.builder()
.body(e.getMessage())
.status(HttpStatus.UNAUTHORIZED.value())
.build(),
HttpStatus.UNAUTHORIZED
);
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class ErrorBody {
private String body;
private int status;
}
参数传递时使用@RequestHeader
注解
Controller代码:
@GetMapping("/{id}")
@CheckLogin
public ShareDTO findById(@PathVariable Integer id,
@RequestHeader("X-Token") String token) {
return this.shareService.findById(id,token);
}
Feign Client接口代码:
@GetMapping("/users/{id}")
UserDTO findById(@PathVariable Integer id,
@RequestHeader("X-Token") String token);
使用Feign的RequestInterceptor
拦截器
定义一个类实现RequestInterceptor接口
public class TokenRelayRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
//1. 从header里面获取token
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes)requestAttributes;
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("X-Token");
//2. 传递token
if (StringUtils.isNoneBlank(token))
requestTemplate.header("X-Token",token);
}
}
配置(这里使用全局配置方式,所有Feign接口都会带上token):
feign:
client:
config:
#全局配置
default:
loggerLevel: BASIC
requestInterceptors:
- com.banmingi.nodeapp.contentcenter.interceptor.TokenRelayRequestInterceptor
调用exchange()
方法
@GetMapping("/tokenRelay/{userId}")
public ResponseEntity<UserDTO> tokenRelay(@PathVariable Integer userId, HttpServletRequest request) {
String token = request.getHeader("X-Token");
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("X-Token",token);
ResponseEntity<UserDTO> exchange = this.restTemplate
.exchange("http://user-center/users/{userId}",
HttpMethod.GET,
new HttpEntity<>(httpHeaders),
UserDTO.class,
userId);
return exchange;
}
使用RestTemplate的ClientHttpRequestIntercept
拦截器
定义一个类实现ClientHttpRequestIntercept接口
public class TestRestTemplateTokenRelayInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
//1. 从header里面获取token
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes)requestAttributes;
HttpServletRequest httpRequest = attributes.getRequest();
String token = httpRequest.getHeader("X-Token");
HttpHeaders headers = request.getHeaders();
headers.add("X-Token",token);
//保证请求继续执行
return execution.execute(request,body);
}
}
配置:
@Bean
@LoadBalanced
//@SentinelRestTemplate
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(
Collections.singletonList(
new TestRestTemplateTokenRelayInterceptor()
));
return restTemplate;
}
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
新建注解
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthorization {
String value();
}
新建AOP切面,实现加有注解的地方需要进行权限验证
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AuthAspect {
private final JwtOperator jwtOperator;
/**
* 检查登录状态
* @param point
* @return
*/
@Around("@annotation(com.banmingi.nodeapp.contentcenter.auth.CheckLogin)")
public Object checkLogin(ProceedingJoinPoint point) throws Throwable {
this.checkToken();
return point.proceed();
}
private void checkToken() {
try {
//1. 从header里面获取token
HttpServletRequest request = getHttpServletRequest();
String token = request.getHeader("X-Token");
//2. 校验token是否合法或在有效期内,如果不合法或已过期,直接抛异常;如果合法或未过期,放行
Boolean isValid = jwtOperator.validateToken(token);
if (!isValid) {
throw new SecurityException("token 不合法!");
}
//3. 如果校验成功,就将用户的信息设置到request的attribute里面
Claims claims = jwtOperator.getClaimsFromToken(token);
request.setAttribute("id",claims.get("id"));
request.setAttribute("wxNickname",claims.get("wxNickname"));
request.setAttribute("role",claims.get("role"));
} catch (Throwable throwable) {
throw new SecurityException("token 不合法!");
}
}
/**
* 获取request
* @return
*/
private HttpServletRequest getHttpServletRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes)requestAttributes;
return attributes.getRequest();
}
/**
* 权限验证
* @param point
* @return
* @throws Throwable
*/
@Around("@annotation(com.banmingi.nodeapp.contentcenter.auth.CheckAuthorization)")
public Object checkAuthorization(ProceedingJoinPoint point) throws Throwable {
try {
//1. 验证token是否合法
this.checkToken();
//2. 验证用户角色是否匹配
HttpServletRequest request = getHttpServletRequest();
String role = (String) request.getAttribute("role");
MethodSignature signature = (MethodSignature) point.getSignature();
//拿到添加@CheckAuthorization注解的方法
Method method = signature.getMethod();
//拿到@CheckAuthorization注解
CheckAuthorization annotation = method.getAnnotation(CheckAuthorization.class);
String value = annotation.value();
if (!Objects.equals(role,value)) {
throw new SecurityException("用户无权访问!");
}
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new SecurityException("用户无权访问!");
}
return point.proceed();
}
}