Springboot + Vue + shiro 實現前後端分離、權限控制

點擊上方 "程序員小樂"關注, 星標或置頂一起成長

每天凌晨00點00分, 第一時間與你相約


每日英文

The sign of maturity is not when you start speaking big things,But, actually it is, When you start understanding Small things.

成熟的標誌不是會說大道理,而是你開始去理解,身邊的小事情。


每日掏心話

你去了很多地方,做了很多事情,認識了很多人。有人相信你,有人質疑你,但你錯了,你一直在不停地向很多人解釋你做過的一些事情。

來自:_Yufan | 責編:樂樂

鏈接:
cnblogs.com/yfzhou/p/9813177.html

Springboot + Vue + shiro 實現前後端分離、權限控制

程序員小樂(ID:study_tech)第 836 次推文 圖片來自百度


往日回顧:基於 token 的多平臺身份認證架構設計


正文

本文總結自實習中對項目的重構。原先項目採用Springboot+freemarker模版,開發過程中覺得前端邏輯寫的實在噁心,後端Controller層還必須返回Freemarker模版的ModelAndView,逐漸有了前後端分離的想法,由於之前,沒有接觸過,主要參考的還是網上的一些博客教程等,初步完成了前後端分離,在此記錄以備查閱。

一、前後端分離思想

前端從後端剝離,形成一個前端工程,前端只利用Json來和後端進行交互,後端不返回頁面,只返回Json數據。前後端之間完全通過public API約定。

二、後端 Springboot

Springboot就不再贅述了,Controller層返回Json數據。

@RequestMapping(value = "/add", method = RequestMethod.POST)
@ResponseBody
public JSONResult addClient(@RequestBody String param) {


JSONObject jsonObject = JSON.parseObject(param);
String task = jsonObject.getString("task");
List list = jsonObject.getJSONArray("attributes");
List attrList = new LinkedList(list);
Client client = JSON.parseObject(jsonObject.getJSONObject("client").toJSONString(),new TypeReference(){});
clientService.addClient(client, task, attrList);
return JSONResult.ok();
}

Post請求使用@RequestBody參數接收。

三、前端 Vue + ElementUI + Vue router + Vuex + axios + webpack

主要參考:

https://cn.vuejs.org/v2/guide/
https://github.com/PanJiaChen/vue-admin-template/blob/master/README-zh.md
https://github.com/PanJiaChen/vue-element-admin

這裡主要說一下開發工程中遇到的問題:

1.跨域

由於開發中前端工程使用webpack啟了一個服務,所以前後端並不在一個端口下,必然涉及到跨域:

XMLHttpRequest會遵守同源策略(same-origin policy). 也即腳本只能訪問相同協議/相同主機名/相同端口的資源, 如果要突破這個限制, 那就是所謂的跨域, 此時需要遵守CORS(Cross-Origin Resource Sharing)機制。

解決跨域分兩種:

1、server端是自己開發的,這樣可以在在後端增加一個攔截器

@Component
public class CommonIntercepter implements HandlerInterceptor {



private final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//允許跨域,不能放在postHandle內
response.setHeader("
Access-Control-Allow-Origin", "*");
if (request.getMethod().equals("OPTIONS")) {
response.addHeader("
Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH");
response.addHeader("
Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
}
return true;
}
}

response.setHeader("Access-Control-Allow-Origin", "*");

主要就是在Response Header中增加 "
Access-Control-Allow-Origin: *"

if (request.getMethod().equals("OPTIONS")) {
response.addHeader("Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH");
response.addHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
}

由於我們在前後端分離中集成了shiro,因此需要在headers中自定義一個'Authorization'字段,此時普通的GET、POST等請求會變成preflighted request,即在GET、POST請求之前會預先發一個OPTIONS請求,這個後面再說。推薦一篇博客介紹 preflighted request。

https://blog.csdn.net/cc1314_/article/details/78272329

2、server端不是自己開發的,可以在前端加proxyTable。

不過這個只能在開發的時候用,後續部署,可以把前端項目作為靜態資源放到後端,這樣就不存在跨域(由於項目需要,我現在是這麼做的,根據網上博客介紹,可以使用nginx,具體怎麼做可以在網上搜一下)。

遇到了網上很多人說的,proxyTable無論如何修改,都沒效果的現象。

1、(非常重要)確保proxyTable配置的地址能訪問,因為如果不能訪問,在瀏覽器F12調試的時候看到的依然會是提示404。

並且注意,在F12看到的js提示錯誤的域名,是js寫的那個域名,並不是代理後的域名。(l樓主就遇到這個問題,後端地址缺少了查詢參數,代理設置為後端地址,然而F12看到的錯誤依然還是本地的域名,並不是代理後的域名)

2、就是要手動再執行一次npm run dev

四、前後端分離項目中集成shiro

可以參考:

blog.csdn.net/u013615903/article/details/78781166

這裡說一下實際開發集成過程中遇到的問題:

1、OPTIONS請求不帶'Authorization'請求頭字段:

前後端分離項目中,由於跨域,會導致複雜請求,即會發送preflighted request,這樣會導致在GET/POST等請求之前會先發一個OPTIONS請求,但OPTIONS請求並不帶shiro的'Authorization'字段(shiro的Session),即OPTIONS請求不能通過shiro驗證,會返回未認證的信息。

解決方法:給shiro增加一個過濾器,過濾OPTIONS請求

public class CORSAuthenticationFilter extends FormAuthenticationFilter {

private static final Logger logger = LoggerFactory.getLogger(
CORSAuthenticationFilter.class);

public CORSAuthenticationFilter() {
super();
}

@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//Always return true if the request's method is OPTIONSif (request instanceof HttpServletRequest) {
if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
return true;
}
}
return super.isAccessAllowed(request, response, mappedValue);
}

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse res = (HttpServletResponse)response;
res.setHeader("
Access-Control-Allow-Origin", "*");
res.setStatus(HttpServletResponse.SC_OK);
res.setCharacterEncoding("UTF-8");
PrintWriter writer = res.getWriter();
Map map= new HashMap<>();
map.put("code", 702);
map.put("msg", "未登錄");
writer.write(JSON.toJSONString(map));
writer.close();
return false;
}
}

貼一下我的config文件:

@Configuration
public class ShiroConfig {

@Bean
public Realm realm() {


return new DDRealm();
}

@Bean
public CacheManager cacheManager() {
return new
MemoryConstrainedCacheManager();
}

/**
* cookie對象;
* rememberMeCookie()方法是設置Cookie的生成模版,比如cookie的name,cookie的有效時間等等。
* @return
*/
@Bean
public SimpleCookie rememberMeCookie(){
//System.out.println("
ShiroConfiguration.rememberMeCookie()");
//這個參數是cookie的名稱,對應前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//
simpleCookie.setMaxAge(259200);
return simpleCookie;
}

/**
* cookie管理對象;
* rememberMeManager()方法是生成rememberMe管理器,而且要將這個rememberMe管理器設置到securityManager中
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager(){
//System.out.println("
ShiroConfiguration.rememberMeManager()");
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();

cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密鑰 建議每個項目都不一樣 默認AES算法 密鑰長度(128 256 512 位)

cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
return cookieRememberMeManager;

}

@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
sm.setRealm(realm());
sm.setCacheManager(cacheManager());
//注入記住我管理器
sm.setRememberMeManager(rememberMeManager());
//注入自定義sessionManager
sm.setSessionManager(sessionManager());
return sm;
}

//自定義sessionManager
@Bean
public SessionManager sessionManager() {
return new CustomSessionManager();
}

public CORSAuthenticationFilter corsAuthenticationFilter(){
return new CORSAuthenticationFilter();
}

@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();

shiroFilter.setSecurityManager(securityManager);

//SecurityUtils.setSecurityManager(securityManager);
Map filterChainDefinitionMap = new LinkedHashMap<>();
//配置不會被攔截的鏈接,順序判斷

filterChainDefinitionMap.put("/", "anon");

filterChainDefinitionMap.put("/static/js/**", "anon");

filterChainDefinitionMap.put("/static/css/**", "anon");

filterChainDefinitionMap.put("/static/fonts/**", "anon");

filterChainDefinitionMap.put("/login/**", "anon");

filterChainDefinitionMap.put("/corp/call_back/receive", "anon");
//authc:所有url必須通過認證才能訪問,anon:所有url都可以匿名訪問


filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");

shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
//自定義過濾器
Map filterMap = new LinkedHashMap<>();
filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter());
shiroFilter.setFilters(filterMap);

return shiroFilter;
}

/**
* Shiro生命週期處理器 * @return
*/
@Bean
public
LifecycleBeanPostProcessor
lifecycleBeanPostProcessor() {
return new
LifecycleBeanPostProcessor();
}

/**
* 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 * 配置以下兩個bean(
DefaultAdvisorAutoProxyCreator(可選)和
AuthorizationAttributeSourceAdvisor)即可實現此功能 * @return
*/
@Bean
@DependsOn({"
lifecycleBeanPostProcessor"})
public
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {

DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new
DefaultAdvisorAutoProxyCreator();

advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}

@Bean
public
AuthorizationAttributeSourceAdvisor

authorizationAttributeSourceAdvisor(SecurityManager securityManager) {

AuthorizationAttributeSourceAdvisor
authorizationAttributeSourceAdvisor = new
AuthorizationAttributeSourceAdvisor();

authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return
authorizationAttributeSourceAdvisor;
}
}

2、設置session失效時間

shiro session默認失效時間是30min,我們在自定義的sessionManager的構造函數中設置失效時間為其他值

public class CustomSessionManager extends DefaultWebSessionManager {

private static final Logger logger = LoggerFactory.getLogger(
CustomSessionManager.class);

private static final String AUTHORIZATION = "Authorization";

private static final String
REFERENCED_SESSION_ID_SOURCE = "Stateless request";

public CustomSessionManager() {
super();
setGlobalSessionTimeout(
DEFAULT_GLOBAL_SESSION_TIMEOUT * 48);
}

@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);//如果請求頭中有 Authorization 則其值為sessionId
if (!StringUtils.isEmpty(sessionId)) {
request.setAttribute(
ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(
ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(
ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
} else {


//否則按默認規則從cookie取sessionId
return super.getSessionId(request, response);
}
}
}

五、部署項目

前端項目部署主要分兩種方法:

1、將前端項目打包(npm run build)成靜態資源文件,放入後端,一起打包。後端寫一個Controller返回前端界面(我使用Vue開發的是單頁面應用),但是這樣其實又將前後端耦合在一起了,不過起碼做到前後端分離開發,方便開發的目的已經達成,也初步達成了要求,由於項目的需要,我是這樣做的,並且免去了跨域問題。

@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
public String index() {
return "/index";
}

2.將前端工程另啟一個服務(tomcat,nginx,nodejs),這樣有跨域的問題。

說一下我遇到的問題:

1、nginx反向代理,導致當訪問無權限的頁面時,shiro 302到unauth的controller,訪問的地址是https,重定向地址是http,導致了無法訪問。

不使用shiro的 shiroFilter.setLoginUrl("/unauth");

當頁面無權限訪問時,我們在過濾器裡直接返回錯誤信息,不利用shiro自帶的跳轉。看過濾器中的onAccessDenied函數

public class CORSAuthenticationFilter extends FormAuthenticationFilter {

private static final Logger logger = LoggerFactory.getLogger(
CORSAuthenticationFilter.class);

public CORSAuthenticationFilter() {
super();
}

@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//Always return true if the request's method is OPTIONS
if (request instanceof HttpServletRequest) {
if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
return true;
}
}
return super.isAccessAllowed(request, response, mappedValue);
}

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse res = (HttpServletResponse)response;
res.setHeader("
Access-Control-Allow-Origin", "*");
res.setStatus(HttpServletResponse.SC_OK);
res.setCharacterEncoding("UTF-8");
PrintWriter writer = res.getWriter();
Map map= new HashMap<>();
map.put("code", 702);
map.put("msg", "未登錄");
writer.write(JSON.toJSONString(map));
writer.close();
return false;
}
}

先記錄這麼多,有不對的地方,歡迎指出!


歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。


猜你還想看


阿里、騰訊、百度、華為、京東最新面試題彙集

從上帝視角看Java如何運行

手把手帶你實現線上環境部署概覽

IDEA-2020.1 版本針對調試器和代碼分析器的改進,值得期待

關注訂閱號「程序員小樂」,收看更多精彩內容
嘿,你在看嗎?


分享到:


相關文章: