SpringSession可以解决在微服务高可用或多个微服务中,JSESSIONID不同的问题
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
application:
name: student-system
session:
store-type: redis
cookie和域
微服务认证的问题:每个微服务都需要做单独的认证,对于用户而言,太麻烦了。
单点登录可以解决微服务的鉴权问题
所谓单点登录,就是用户在多系统环境下,在一个单一的服务中登录,进而实现在多个系统同时登录的一种技术。
后端通过JWT的技术,可以生成一个Token令牌的东西,生成出来之后,交给前端,要求前端进行存储,前端的每次请求需要携带该Token令牌到后端来识别
JWT由3段信息构成:头(header)、载荷(payload)、签证(signature)
头(header)、载荷(payload)采用Base64位加密方法,签证(signature)使用HMACSHA256进行加密运算
头(Header)
主要承载2部分内容:声明Token使用JWT技术产生,声明加密的算法通常直接使用HMACSHA256
完整头的JSON格式:
{
'typ': 'JWT',
'alg': 'HS256'
}
头的内容是固定的,不会发生变化
对应的字符串是:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
载荷(payload)
签证(signature)
ClaimJwtException 获取Claim异常
ExpiredJwtException token过期异常
IncorrectClaimException token无效
MalformedJwtException 密钥验证不一致
MissingClaimException JWT无效
RequiredTypeException 必要类型异常
SignatureException 签名异常
UnsupportedJwtException 不支持JWT异常
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
public class JwtUtil {
/**
* 私钥密码,保存在服务器,客户端是不会知道密码的,以防止被攻击
*/
private static final String SECRET = "mywebsite";
/**
* 加密方式
*/
private static final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
/**
* 对密钥进行加密
* @return
*/
private static Key getkey() {
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
return new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
}
/**
* 生成JWT
* @param subject 主体
* @param expireSeconds 过期时间(second),-1为无限期
* @param id jwt-id
* @return
*/
public static String createToken(String subject,int expireSeconds,String id) {
JwtBuilder builder = Jwts.builder();
// 设置头部
Map<String, Object> map = new HashMap<String, Object>();
map.put("alg", "HS256");
map.put("typ", "JWT");
builder.setHeaderParams(map);
// 设置载荷
builder.setSubject(subject);
builder.setId(id);
// 设置密钥
builder.signWith(signatureAlgorithm, getkey());
// 设置签发时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
builder.setIssuedAt(now);
// 设置过期时间
if (expireSeconds >= 0) {
long expMillis = nowMillis + (expireSeconds + 2) * 1000;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate);
}
String token = builder.compact();
return token;
}
/**
* 解密Token查看其是否合法
*/
public static boolean verifyToken(String token) {
Claims body = null;
try {
body = Jwts.parser().setSigningKey(getkey()).parseClaimsJws(token).getBody();
}catch (Exception e){
return false;
}
return body != null;
}
/**
* 解析载荷中主体和jwt-id
* @param token
* @return sub+id
*/
public static Map<String,String> getPayload(String token) {
boolean b = verifyToken(token);
if (!b){
return null;
}
Claims body = Jwts.parser().setSigningKey(getkey()).parseClaimsJws(token).getBody();
Map<String,String> payLoad = new HashMap<>();
payLoad.put("id",(String) body.get("jti"));
payLoad.put("sub",(String) body.get("sub"));
return payLoad;
}
}
<dependencies>
<!-- 公共模块,包含mysql依赖 -->
<dependency>
<groupId>top.ygang</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
server:
port: 8888
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?serverTimezone=UTC&characterEncoding=utf8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML5
cache: false
encoding: UTF-8
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login">
<input type="hidden" name="backUrl" th:value="${backUrl}">
account<input name="account"><br>
psw<input name="psw"><br>
<button>sub</button>
</form>
</body>
</html>
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Resource
private UserInfoDao userInfoDao;
@Override
public UserInfo login(String account, String psw) {
UserInfoExample userInfoExample = new UserInfoExample();
UserInfoExample.Criteria criteria = userInfoExample.createCriteria();
criteria.andAccountEqualTo(account);
criteria.andPswEqualTo(psw);
List<UserInfo> userInfos = userInfoDao.selectByExample(userInfoExample);
return userInfos.size() > 0 ? userInfos.get(0) : null;
}
}
@Controller
public class LoginController {
@Resource
private UserInfoService userInfoService;
/**
* 跳转登录页面方法,同时验证token,验证成功,则跳转回原来的地址
* backUrl,为测试用的原来的地址
*/
@RequestMapping("/toLogin")
public String toLogin(String backUrl, @CookieValue(value = "token",required = false)String token, ModelMap map){
//判断令牌是否有效
if (!StringUtils.isEmpty(token)){
boolean b = JwtUtil.verifyToken(token);
if (b){
return "redirect:" + backUrl + "?token=" + token;
}
}
//令牌失效或不存在
map.put("backUrl",backUrl);
//跳转登录页面
return "login";
}
/**
* 登录验证方法
*/
@RequestMapping("/login")
public String login(UserInfo userInfo, String backUrl, HttpServletResponse response){
//验证用户的登录
UserInfo loginUser = userInfoService.login(userInfo.getAccount(),userInfo.getPsw());
//如果账号密码正确,则创建令牌
if (loginUser != null){
String token = JwtUtil.createToken(loginUser.getAccount(), 3000,nul);
//将令牌存入cookie
Cookie cookie = new Cookie("token",token);
response.addCookie(cookie);
return "redirect:"+backUrl+"?token=" + token;
}
return "redirect:http://localhost:8888/toLogin?backUrl=" + backUrl;
}
}
@Component
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取用户实际请求的地址,用于令牌验证通过继续访问
String backUrl = request.getRequestURL().toString();
//从参数中获取token
String token_r = request.getParameter("token");
//从cookie中获取token
Cookie cookie = WebUtils.getCookie(request, "token");
String token_c = cookie == null ? null : cookie.getValue();
//从参数或cookie的token中选择有效的token
String token = StringUtils.isEmpty(token_r)?token_c : token_r;
//验证token
if (!StringUtils.isEmpty(token)){
if (JwtUtil.verifyToken(token)){
Cookie cookie1 = new Cookie("token",token);
response.addCookie(cookie);
return true;
}
}
//验证不通过,跳转登录页面
response.sendRedirect("http://localhost:8888/toLogin?backUrl=" + backUrl);
return false;
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration registration = registry.addInterceptor(loginInterceptor);
registration.addPathPatterns("/**"); //所有路径都被拦截
registration.excludePathPatterns( //添加不拦截路径
"/**/*.html", //html静态资源
"/**/*.js", //js静态资源
"/**/*.css", //css静态资源
"/**/*.woff",
"/**/*.ttf"
);
}
}
由于简单使用中,在每个微服务都添加一个拦截器并进行配置,过于繁琐,所以,我们在Gateway上直接添加全局的过滤器用于SSO,所有的微服务都由Gateway进行统一的管理转发
令牌的产生是在具有登录服务的微服务上,而令牌的校验是在网关服务上对其进行校验
server:
port: 80
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
gateway:
discovery:
locator:
# 让gateway从nacos中获取服务信息
enabled: false
routes:
# 路由标识,必须唯一,默认是UUID
- id: student-system
# 路由的目标地址,lb(loadblance)表示使用负载均衡,其后是微服务的标识
uri: lb://student-system
# 路由的优先级,数字越小代表路由的优先级越高
order: 1
# 当请求路径匹配Path时,就会路由到uri
predicates:
- Path=/student-system/**
# 转发之前,去掉1层路径
filters:
- StripPrefix=1
- id: clazz-system
uri: lb://clazz-system
order: 1
predicates:
- Path=/clazz-system/**
filters:
- StripPrefix=1
@Component
public class LoginGlobalFilter implements GlobalFilter, Ordered {
//认证中心的跳转登录页面Url
private final String loginServerUrl = "http://localhost:8888/toLogin";
//当前Gateway的Url
private final String gatewayUrl = "http://localhost";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//获取本来访问的路径
String backUrl = gatewayUrl + request.getPath().toString();
//获取参数中的token
String token_r = request.getQueryParams().getFirst("token");
//获取cookie中的token
String token_c = getCookieByName(request, "token");
//在参数或cookie中的token选择有效的token
String token = StringUtils.isEmpty(token_r) ? token_c : token_r;
//如果token为空
if (StringUtils.isEmpty(token)){
return gotoLogin(response,backUrl);
}
//如果token无效
if (!JwtUtil.verifyToken(token)){
return gotoLogin(response,backUrl);
}
//其他情况放行
return chain.filter(exchange);
}
//前往认证微服务进行认证
private Mono<Void> gotoLogin(ServerHttpResponse response, String backUrl) {
//设置状态码
response.setStatusCode(HttpStatus.SEE_OTHER);
//设置响应头,重定向到认证中心
response.getHeaders().add(HttpHeaders.LOCATION,loginServerUrl + "?backUrl=" + backUrl);
return response.setComplete();
}
public String getCookieByName(ServerHttpRequest request,String cookieName){
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
Set<Map.Entry<String, List<HttpCookie>>> entries = cookies.entrySet();
for (Map.Entry<String, List<HttpCookie>> entry : entries) {
if (entry.getKey().equals(cookieName)) {
List<HttpCookie> value = entry.getValue();
if (value.size() > 0) {
return value.get(0).getValue();
}
}
}
return null;
}
@Override
public int getOrder() {
return 0;
}
}
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......造成的问题包括:个人隐私泄露以及财产安全。
CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com、Metafilter(一个大型的BLOG网站),YouTube和百度HI......而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。
登录受信任网站A,并在本地生成Cookie。
在不登出A的情况下,访问危险网站B。
看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。
你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了......)
上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。