背景:
最近公司新接了一個小項目,在下有幸承擔了後臺開發的所有部分。所以基於以上環境,我開始着手搭建了一個以springboot爲基礎的項目,其中包含了整合shiro。
開發環境:
springboot版本1.5.9
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/>
</parent>
shiro版本是shiro-spring1.4.0
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
項目介紹:
由於上面提到項目是個小項目,所以搭建分佈式或者是微服務架構都是一件費工費時出力不討好的事情,而且自己能力有限,目前項目搭建的是單應用架構,是前後端分離,前端用的vue但不是我來寫。
開始:
1.引入依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2.自定義realm(如果shiro的原理以及執行流程概念還模糊的建議先梳理一下shiro框架的流程,在看這篇文章會更清晰。)
realm是用來用戶認證以及權限鑑定的,所以我們要繼承AuthorizingRealm類,然後重寫doGetAuthorizationInfo和doGetAuthenticationInfo兩個方法。doGetAuthorizationInfo是進行權限認證;doGetAuthenticationInfo是用來用戶認證。
代碼如下:
package com.iterge.config;
import com.iterge.entity.SysPermission;
import com.iterge.entity.SysRole;
import com.iterge.entity.User;
import com.iterge.service.SysPermissionService;
import com.iterge.service.SysRoleService;
import com.iterge.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* @Description 自定義權限匹配和賬號密碼匹配
* @Author iterge
* @Date 2019/6/12 14:02
*/
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysPermissionService sysPermissionService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("權限認證");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principal.getPrimaryPrincipal();
System.out.println("**********"+user.toString());
try {
List<SysRole> roles = sysRoleService.selectByUid(user.getUid());
for (SysRole role : roles) {
authorizationInfo.addRole(role.getRolename());
}
List<SysPermission> permissions = sysPermissionService.selectByUid(user.getUid());
for(SysPermission permission : permissions){
authorizationInfo.addStringPermission(permission.getPername());
}
}catch (Exception e){
e.printStackTrace();
}
return authorizationInfo;
}
/*主要是用來進行身份認證的,也就是說驗證用戶輸入的賬號和密碼是否正確。*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//獲取用戶賬號和密碼
System.out.println("用戶認證");
String username = (String) authenticationToken.getPrincipal();
User user = userService.login(username);
if(user == null){
return null;
}
if (user.getStatus() == 1) { //賬戶凍結
throw new LockedAccountException();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user,//安全數據
user.getPassword(),//
getName()
);
return authenticationInfo;
}
}
2.自定義SessionManager,爲什麼要自定義這個東東呢?其實不前後端分離的情況下Shiro自身提供的sessionid+cookie的機制是能滿足傳統單應用系統的需求的。但是注意,咱們現在是前後端分離,ajax請求時不會像傳統系統那樣,會記住服務器端傳過去的sessionid,那我們就要想個辦法怎麼讓每次請求都記住這個sessionid並傳給後臺,讓後臺知道,每次請求都是被認證過的同一個人,所以我們這裏就採用自定義SessionManager的方式自己來管理sessionid的獲取,這樣前端需要做的就是每次請求,要把後端傳給它的sessionid即token,放到請求頭裏key爲Authorization,value爲後臺傳過來的token,然後用自定義的SessionManager獲取就ok了。
package com.iterge.config;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
/**
* @Description 自定義SessionManager
* @Author iterge
* @Date 2019/6/13 18:58
*/
public class MySessionManager extends DefaultWebSessionManager {
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
String token = httpServletRequest.getHeader("Authorization");
System.out.println("Authorization:"+token);
if(!StringUtils.isEmpty(token)){
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "Stateless request");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return token;
}else {
return super.getSessionId(request, response);
}
}
}
3.跨域配置,前後端分離,會有兩個服務,一個啓動後端,一個啓動前端,所以要進行跨域配置。(當然如果要把前端項目作爲後端的靜態資源管理起來,也不需跨培配置)
package com.iterge.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;
/**
* @Description 跨域設置
* @Author iterge
* @Date 2019/6/6 18:50
* addMapping("/**") 對接口配置跨域設置 /**代表所有接口
* allowedHeaders("*") 允許所有的請求頭
* allowedMethods("*") 允許所有的方法 也可以設置爲("POST", "GET", "PUT", "OPTIONS", "DELETE")
* allowedOrigins("*") 允許所有的域(源地址)
*/
@Configuration
@Slf4j
public class WebMvcConfig{
/*@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("*")
.allowedMethods("*")
.allowedOrigins("http://192.168.1.163:8890")
.allowCredentials(true);
}*/
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
log.info("**********WebMvcConfig**********");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}
/*
* 跨域過濾器
* @return
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
4.自定義過濾器。這個過濾器要特殊說明下,實際生產環境中可能,有些同學不配置這個過濾器時,會出現302的錯誤,這裏說明一下爲什麼。瀏覽器請求接口時,會先發送一個"OPTIONS"的預請求,只有這個預請求返回的狀態是200時,纔會進行真正的請求。如果不把這個預請求過濾一下,就會出現302的錯誤,所以我們要把這個"OPTIONS"方法的預請求過濾下。
package com.iterge.config;
import com.alibaba.fastjson.JSON;
import com.iterge.entity.Result;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* @Description
* @Author iterge
* @Date 2019/6/14 14:28
*/
public class CORSAuthenticationFilter extends FormAuthenticationFilter {
public CORSAuthenticationFilter() {
super();
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(request instanceof HttpServletRequest){
if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")){
System.out.println("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");
res.setContentType("text/html; charset=utf-8");
PrintWriter writer = res.getWriter();
Map map = new HashMap();
map.put("code", Result.NOTLOGIN.getCode());
map.put("msg", Result.NOTLOGIN.getMsg());
writer.write(JSON.toJSONString(map));
writer.close();
return false;
}
}
定義好以後在shiro的配置類中使用。
5.shiro配置:
package com.iterge.config;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Description shiro配置文件
* @Author iterge
* @Date 2019/6/10 10:23
*/
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filterChainDefinitionMap = new LinkedHashMap();
//注意過濾器配置順序 不能顛倒
//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了,登出後跳轉配置的loginUrl
//filterChainDefinitionMap.put("/logout", "logout");
// 配置不會被攔截的鏈接 順序判斷
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger-resources", "anon");
filterChainDefinitionMap.put("/swagger-resources/configuration/security", "anon");
filterChainDefinitionMap.put("/swagger-resources/configuration/ui", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/webjars/springfox-swagger-ui/**", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
//filterChainDefinitionMap.put("/**", "authc");
filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
//配置shiro默認登錄界面地址,前後端分離中登錄界面跳轉應由前端路由控制,後臺僅返回json數據
//shiroFilterFactoryBean.setLoginUrl("/unauth");
//shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//自定義過濾器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter());
shiroFilterFactoryBean.setFilters(filterMap);
return shiroFilterFactoryBean;
}
@Bean
public MyShiroRealm myShiroRealm(){
return new MyShiroRealm();
}
@Bean
public SecurityManager securityManager(MyShiroRealm realm,SessionManager sessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean
public SessionManager sessionManager(){
System.out.println("******sessionManager()");
return new MySessionManager();
}
public CORSAuthenticationFilter corsAuthenticationFilter(){
return new CORSAuthenticationFilter();
}
/**
* 開啓shiro aop註解支持.
* 使用代理方式;所以需要開啓代碼支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 註冊全局異常處理
* @return
*/
@Bean(name = "exceptionHandler")
public HandlerExceptionResolver handlerExceptionResolver() {
return new MyExceptionHandler();
}
}
最後:
附上全局異常的代碼:
package com.iterge.config;
import com.alibaba.fastjson.support.spring.FastJsonJsonView;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @Description 自定義異常
* @Author iterge
* @Date 2019/6/12 15:58
*/
public class MyExceptionHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception ex) {
ModelAndView mv = new ModelAndView();
FastJsonJsonView view = new FastJsonJsonView();
Map<String, Object> attributes = new HashMap<String, Object>();
if (ex instanceof UnauthenticatedException) {
attributes.put("code", "1000001");
attributes.put("msg", "token錯誤");
} else if (ex instanceof UnauthorizedException) {
attributes.put("code", "1000002");
attributes.put("msg", "用戶無權限");
} else {
attributes.put("code", "1000003");
attributes.put("msg", ex.getMessage());
}
view.setAttributesMap(attributes);
mv.setView(view);
return mv;
}
}
如果有什麼不明白或者不足的地方歡迎提問和指出。