兜兜轉轉,轉眼已經進入12月中旬了,馬上就是2020年了,回顧這一年,技術上,真的提升很少很少,項目中使用的技術都是很老套的SpringMvc+MyBatis,有的老項目還使用的是Struts2+Hibernate,公司對技術也不重視,在穩定的基礎上,不求有功,但求無過;也沒有什麼技術分享。我經歷過上家公司的快速節奏,現在也放鬆下來了,去年我還自學python、學習數據結構和算法、碰到的技術問題都會去深究。今年,雖然還保持博客輸出,但是,明顯懈怠了,基本是工作中碰到的問題纔會輸出,問題的根本原因,除了那些比較明顯的,會寫一寫;隱藏的比較深的那些問題,都是沒有下文的。
上個月,老大說老闆要搞聚合支付,然後不知道從哪弄來的一套支付代碼,讓我們幾個開發熟悉代碼,爲將來的開發任務做準備。這項目使用了SpringCloud的很多組件,spring-cloud-bus、spring-cloud-consul、spring-cloud-feign、spring-cloud-hystrix等等,項目被拆分爲9個相互關聯的小項目,環境搭建還是使用的docker技術,別的不說,光是能在本地把項目跑起來,前前後後都花了近一天時間。
正好閒了快一年了,又燃起了對技術的熱枕,準備把這支付項目使用的技術,都整理輸出。第一篇輸出是Shiro。爲什麼是Shiro呢?因爲權限驗證,是網站的基礎,它是用戶維度下,用戶跟網站關聯的第一步,所以,它是第一個輸出。
理論部分
Shiro是什麼?
Shiro是一款簡單易用的java安全框架,提供了認證、授權、會話管理等功能,由Apache組織開源,在業內被廣泛使用。官網是: http://shiro.apache.org
核心組件
核心組件有三個,分別是Subject、Realm、SecurityManager。
Subject:當前操作的主體,可以是自然人,也可以是爬蟲。比如,如果張三在網站通過登陸頁面登陸,進入系統,那當前操作的主體就是張三;如果是爬蟲S模擬登陸、進入系統,那當前操作的主體就是S。
Realm:域,對登陸時的身份驗證、對訪問的權限控制,這些都是要由我們自己來實現。
SecurityManager:Shiro框架中,調節者的功能,充當大主管,各種“瑣事”都要交給它,比如,實現的realm要交給SM(SecurityManager下面統一簡稱SM)來管理;使用緩存管理器記錄用戶權限信息,也要交給SM;開啓“記住我”功能,也要通知SM。
再附上Shiro的完整架構圖
實踐部分
上面都是說的理論,說的不是非常全面,想了解更多的,可以自行百度或者谷歌。
1.數據庫支持
我這裏使用的是MySQL,建表語句,這裏就不貼出來了,太長了。 可以戳我下載
數據持久層使用的是MyBatis框架,對應表的XML文件,都是自動生成的。
2.引入Maven依賴
這裏使用的是SpringBoot,對SpringBoot不瞭解的也沒關係,就當它是一個普通Spring項目,只不過引入了特定的依賴。
這裏是繼承 spring-boot-starter-parent 的方式來構建SpringBoot應用。
<dependencies>
<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>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
</dependencies>
3.ShiroConfig配置
顧名思義,ShiroConfig就是Shiro的配置相關,比如:定義SM、定義攔截和不攔截的url、添加註解支持、添加緩存支持等,具體代碼如下:
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 設置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登錄的url
shiroFilterFactoryBean.setLoginUrl("/login");
// 登錄成功後跳轉的url
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授權url
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 定義filterChain,靜態資源不攔截
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
// druid數據源監控頁面不攔截
filterChainDefinitionMap.put("/druid/**", "anon");
// 配置退出過濾器,其中具體的退出代碼Shiro已經替我們實現了
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/", "anon");
// 除上以外所有url都必須認證通過纔可以訪問,未通過認證自動訪問LoginUrl
// filterChainDefinitionMap.put("/**", "authc"); // authc表示:需要通過認證
filterChainDefinitionMap.put("/**", "user"); // user表示:通過認證,或者,remeber me記住了登錄狀態
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/** * 安全管理器,Shiro的大主管,各種”瑣事“處理都要委託給它<br/> * 比如設置realm域、設置“記住我”、設置緩存等 * * @return */
@Bean
public SecurityManager securityManager() {
// 配置SecurityManager,並注入shiroRealm
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager(getEhCacheManager());
return securityManager;
}
/** * 自己實現的Realm * * @return */
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
/** * Shiro註解支持 * <li>表示當前Subject已經通過login進行了身份驗證;即Subject.isAuthenticated()返回true。 @RequiresAuthentication </li> * <li>表示當前Subject已經身份驗證或者通過記住我登錄的。 @RequiresUser </li> * <li>表示當前Subject沒有身份驗證或通過記住我登錄過,即是遊客身份。 @RequiresGuest </li> * <li>表示當前Subject需要角色admin和user。 @RequiresRoles(value={"admin", "user"}, logical= Logical.AND)</li> * <li>表示當前Subject需要權限user:a或user:b。 @RequiresPermissions (value={"user:a", "user:b"}, logical= Logical.OR)</li> * * @param securityManager * @return */
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/** * 緩存支持,這裏是ehcache * * @return */
@Bean
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:config/shiro-ehcache.xml");
return em;
}
/** * 返回cookie管理對象 * <li>1.默認cookie對象設置</li> * <li>2.放入cookie管理器</li> * * @return */
private CookieRememberMeManager rememberMeManager() {
// 設置cookie名稱,對應login.html頁面的<input type="checkbox" name="rememberMe"/>
SimpleCookie cookie = new SimpleCookie("rememberMe");
// 設置cookie的過期時間,單位爲秒,這裏爲一天
cookie.setMaxAge(86400);
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(cookie);
// rememberMe cookie加密的密鑰
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
}
4.realm配置
該類需要繼承 AuthorizingRealm ,需要實現兩個方法,一個是 doGetAuthenticationInfo(),它是登錄認證(登錄時調用),驗證用戶名和密碼;另外一個是 doGetAuthorizationInfo() ,它是獲取用戶角色和權限(訪問控制),獲取用戶的權限,對訪問的放行或者阻攔。
具體代碼如下:
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private UserPermissionMapper userPermissionMapper;
/** * 獲取用戶角色和權限(驗證權限時調用) */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
String userName = user.getUserName();
System.out.println("用戶" + userName + "獲取權限-----ShiroRealm.doGetAuthorizationInfo");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 獲取用戶角色集
List<Role> roleList = userRoleMapper.findByUserName(userName);
Set<String> roleSet = new HashSet<String>();
for (Role r : roleList) {
roleSet.add(r.getName());
}
simpleAuthorizationInfo.setRoles(roleSet);
// 獲取用戶權限集
List<Permission> permissionList = userPermissionMapper.findByUserName(userName);
Set<String> permissionSet = new HashSet<String>();
for (Permission p : permissionList) {
permissionSet.add(p.getName());
}
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
/** * 登錄認證(登錄時調用) */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 獲取用戶輸入的用戶名和密碼
String userName = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
// 通過用戶名到數據庫查詢用戶信息
User user = userMapper.findByUserName(userName);
if (user == null) {
throw new UnknownAccountException("用戶名或密碼錯誤!");
}
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("用戶名或密碼錯誤!");
}
if (user.getStatus().equals("0")) {
throw new LockedAccountException("賬號已被鎖定,請聯繫管理員!");
}
return new SimpleAuthenticationInfo(user, password, getName());
}
}
5.前端頁面和對應controller
1.login.html 登陸頁面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄</title>
<link rel="stylesheet" th:href="@{/css/login.css}" type="text/css">
<script th:src="@{/js/jquery-1.11.1.min.js}"></script>
</head>
<body>
<div class="login-page">
<div class="form">
<input type="text" placeholder="用戶名" name="username" required="required"/>
<input type="password" placeholder="密碼" name="password" required="required"/>
<p><input type="checkbox" name="rememberMe"/>記住我</p>
<button onclick="login()">登錄</button>
</div>
</div>
</body>
<script th:inline="javascript">
var ctx = '/';
function login() {
var username = $("input[name='username']").val();
var password = $("input[name='password']").val();
var rememberMe = $("input[name='rememberMe']").is(':checked');
$.ajax({
type: "post",
url: ctx + "login",
data: {"username": username, "password": password, "rememberMe": rememberMe},
dataType: "json",
success: function (r) {
if (r.code == 0) {
location.href = ctx + 'index';
} else {
alert(r.msg);
}
},
error: function (r) {
console.log("-------------" + r);
}
});
}
</script>
</html>
2.LoginController 登陸控制器
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
@PostMapping("/login")
@ResponseBody
public R login(String username, String password, Boolean rememberMe) {
// 密碼MD5加密
password = MD5Utils.encrypt(username, password);
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
// 獲取Subject對象
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return R.ok();
} catch (UnknownAccountException e) {
return R.error(e.getMessage());
} catch (IncorrectCredentialsException e) {
return R.error(e.getMessage());
} catch (LockedAccountException e) {
return R.error(e.getMessage());
} catch (AuthenticationException e) {
return R.error("認證失敗!");
}
}
@RequestMapping("/")
public String redirectIndex() {
return "redirect:/index";
}
@RequestMapping("/index")
public String index(Model model) {
// 登錄成後,即可通過Subject獲取登錄的用戶信息
User user = (User) SecurityUtils.getSubject().getPrincipal();
model.addAttribute("user", user);
return "index";
}
@GetMapping("/403")
public String forbid() {
return "403";
}
}
6.驗證
啓動項目,訪問login頁面,我本地是 http://localhost:8888/login ,可以看到,輸入用戶名和密碼(用戶名、密碼都是test)後,跳進了首頁