实际开发中,我们的项目大多数都采用前后端分离或者集群的方式部署。在这些情况下,往往会有些问题,比如前后端分离的跨域问题、跨域携带cookie的问题、分布式应用下的session共享等问题,虽然这些问题都能得到相应的解决,但总是觉得没有直接使用token或者jwt方便。接下来我们来对我们目前实现的shiro功能进行一些修改,让它能直接使用token的方式进行认证。
@RequestMapping("login")
public MyResponse login(@RequestBody MyUser myUser){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(myUser.getUserName(), myUser.getPassword());
usernamePasswordToken.setRememberMe(true);
try {
subject.login(usernamePasswordToken);
} catch (LockedAccountException e) {
return MyResponse.error("用户被锁定");
} catch (AuthenticationException e){
return MyResponse.error("用户名密码错误");
}
return MyResponse.success("登录成功",subject.getSession().getId());
}
登录后直接返回sessionId(也就是我们的token)给前端,当然你也可以通过其他方式给前端提供token。
@RestController
@RequestMapping("testSession")
public class TestSessionController {
@RequestMapping("getSession")
public MyResponse getSession(){
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
return MyResponse.success(session);
}
}
除了获取session,我们还可以通过subject.getPrincipal()获取我们的登录用户信息。
当然JQuery或直接AJAX请求也可以
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script src="./js/axios.min.js">script>
head>
<body>
<button onclick="getToken()" type="button">获取tokenbutton>
<button onclick="checkSession()" type="button">验证sessionbutton>
<script>
axios.defaults.withCredentials=false
axios.defaults.crossDomain=true
axios.defaults.headers.post['Content-Type'] = 'application/json'
var baseUrl = "http://localhost:8099/shiro1";
var token = '';
var request = axios.create({
baseURL: baseUrl, // api的base_url
timeout: 50000, // request timeout
data: {}
});
function getToken() {
request({
url: '/login/login',
method: 'post',
data : {
userName: 'anonymous',
password: '123456'
}
}).then(response => {
console.log(response.data)
token = response.data.content;
});
}
function checkSession() {
console.log(token)
request({
url: '/testSession/getSession',
method: 'post',
headers:{
'X-Token':token
}
}).then(response => {
console.log(response.data)
});
}
script>
body>
html>
点击下载axios.min.js文件
在使用shiro-web的情况下,许多解决跨域问题的办法会不生效(比如我之前使用的拦截器的方式或springBoot的方式),我们此处采用Filter的方式来解决跨域的问题
CorsFilter定义如下:
package com.yyoo.mytest.shiro1.config;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 使用shiro的情况下,拦截器来处理跨域有时候不生效
* 建议使用Filter方式处理跨域
*/
@Component
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
// String origin = request.getHeader("Origin");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS");
response.setHeader("Access-Control-Max-Age", "86400");
// 为true时Access-Control-Allow-Origin不能为*
response.setHeader("Access-Control-Allow-Credentials", "false");
// 此处为允许请求携带的所以请求头,如果要限制,可自行定义
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
filterChain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
shiro默认是使用的cookie来实现的登录认证,这意味着我们前后端分离的项目需要设置Access-Control-Allow-Credentials为true,并且前端ajax需要设置携带cookie请求。也就是axios.defaults.withCredentials=true,才行。我们这里由于使用token方式,所以我们的Access-Control-Allow-Credentials设置为false,这意味着我们的每个请求都是不同的请求会话。
其获取Session的定义如下
Session readSession(Serializable sessionId) throws UnknownSessionException;
这里需要一个sessionId,这个sessionId是通过DefaultWebSessionManager获取的。查看源码,我们会找到如下方法:
public Serializable getSessionId(SessionKey key) {
Serializable id = super.getSessionId(key);
if (id == null && WebUtils.isWeb(key)) {
ServletRequest request = WebUtils.getRequest(key);
ServletResponse response = WebUtils.getResponse(key);
id = this.getSessionId(request, response);
}
return id;
}
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
return this.getReferencedSessionId(request, response);
}
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
String id = this.getSessionIdCookieValue(request, response);
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie");
} else {
id = this.getUriPathSegmentParamValue(request, "JSESSIONID");
if (id == null) {
String name = this.getSessionIdName();
id = request.getParameter(name);
if (id == null) {
id = request.getParameter(name.toLowerCase());
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "url");
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, this.isSessionIdUrlRewritingEnabled());
return id;
}
shiro 先执行getSessionId(SessionKey key) ,如果是web应用它将执行getSessionId(ServletRequest request, ServletResponse response),其实就是执行getReferencedSessionId
getReferencedSessionId方法信息量很大,总结起来就是,它会先从请求携带的cookie中获取sessionId,如果为空,则会获取请求参数JSESSIONID的值,但我们可以看到,该实现没有获取请求头的操作,所以我们将自己实现。
其中JSESSIONID这个名称是cookie的名称,这个cookie在DefaultWebSessionManager实例化的时候创建,该名称可以修改(自行实例化一个cookie来传入DefaultWebSessionManager实例即可。)
package com.yyoo.mytest.shiro1.config;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
public class MyWebSessionManager extends DefaultWebSessionManager {
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
Serializable sessionId = req.getHeader("X-Token");
if(sessionId != null){
return sessionId;
}
// 如果消息头获取为空,则使用shiro原来的方式获取
return super.getSessionId(request, response);
}
}
注意:我们此处读取的请求头信息为X-Token,请与前端对应。
@Bean
public SecurityManager securityManager(Realm realm) {
// 注意:这里的DefaultWebSecurityManager和我们之前的Demo使用的DefaultSecurityManager有区别
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 修改web环境下的默认sessionManager
DefaultWebSessionManager sessionManager = new MyWebSessionManager();
// 60分钟(此设置会覆盖容器(tomcat)的会话过期时间设置)
sessionManager.setGlobalSessionTimeout(60*60*1000);
securityManager.setSessionManager(sessionManager);
// 禁用cookie来存sessionID(我们这里不用禁用,如果不传Token,则会使用原方式认证)
// sessionManager.setSessionIdCookieEnabled(false);
log.info("myShiro SessionManager:{}",securityManager.getSessionManager());
log.info("myShiro SessionListeners:{}",sessionManager.getSessionListeners());
log.info("myShiro SessionDAO:{}",sessionManager.getSessionDAO());
securityManager.setRealm(realm);
return securityManager;
}
其实就修改了DefaultWebSessionManager的实现为MyWebSessionManager,然后新增了sessionManager.setSessionIdCookieEnabled(false);而已
以上修改完成后启动程序,登录没有问题,但是获取session调用的时候会出现如下错误:
Access to XMLHttpRequest at 'http://localhost:8099/shiro1/testSession/getSession' from origin 'http://localhost:63342' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
其根本原因就是前端跨域请求时,如果是复杂的跨域请求,前端会先发送一个OPTIONS请求来确认是否能够访问,但是OPTIONS请求有个问题,我们自定义的请求头x-token无法获取值,这导致该请求进入shiro认证时无法通过认证。这就意味着,如果是OPTIONS请求,我们需要跳过认证。
找到问题所在了,接下来就解决它把。
package com.yyoo.mytest.shiro1.config;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
if("OPTIONS".equals(req.getMethod())){
return true;
}
return super.onAccessDenied(request, response);
}
}
// shiro的authc过滤器(注意这些过滤器是有顺序的,此map类型为LinkedHashMap)
shiroFilterFactoryBeanFilters.put("authc",new MyFormAuthenticationFilter());
其实就是修改ShiroFilterFactoryBean配置中的过滤器配置将FormAuthenticationFilter修改为我们重新实现的MyFormAuthenticationFilter即可。
重启后,再次尝试结果图如下
到此为止,我们的shiro使用token实现就完成了。但还有些后续的问题,比如接JWT,比如分布式环境下的操作。
我们在007-shiro的会话管理一章说到,我们可以自定义sessionID,只需实现SessionIdGenerator接口即可,SessionIdGenerator默认使用java的uuid实现,我们当前的token其实就是uuid,那么我们自定义实现用JWT来实现sessionId的生成即可。
现在我们知道,shiro获取session的时候是通过sessionId来直接获取的,而我们的SessionDAO默认是MemorySessionDAO,如果我们自定义实现SessionDAO,将session存储在redis,那么我在分布式环境下,只要sessionID不变(本文其实就是token),通过sessionDao获取redis中存储的session就不会变,这样就轻松实现了session的分布式共享。
在前后端分离的情况下,我们在请求的时候如果未登录或登录过期,shiro是会跳转到我们设置的登录页面的,我们目前设置的登录页面如下
shiroFilterFactoryBean.setLoginUrl("https://blog.csdn.net/forlinkext/article/details/115748941");
在axios请求下会有如下问题
Access to XMLHttpRequest at 'https://blog.csdn.net/forlinkext/article/details/115748941' (redirected from 'http://localhost:8099/shiro1/testSession/getSession') from origin 'http://localhost:63342' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
我们可以将登陆页面地址设置为一个后端的不验证登录的地址,然后该地址直接返回未登录的状态给前端,前端axios统一处理该状态(跳转到登陆页)即可。
由于时间原因,shiro使用JWT和session共享,没有详细编写,只提了基本思路,后续如果有空实现的话我们再来编写把。
上一篇:007-shiro的会话管理