1. 程式人生 > >SSM整合系列之 整合Shiro實現登陸認證

SSM整合系列之 整合Shiro實現登陸認證

前言:Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理,本文將介紹Spring整合Shiro實現登陸認證功能。對Shiro需要更深入的瞭解,請自學(一個禮貌的微笑)本人後續也會詳細總結Shiro。繼本文之後我也將繼續完善登陸的RemberMe功能,以及Shiro+Memcache(也可以是其他如Redis)實現分散式Session共享功能
首先簡單瞭解下Shiro的三個核心元件:Subject, SecurityManager 和 Realms。
  Subject:即“當前操作使用者”。但是,在Shiro中,Subject這一概念並不僅僅指人,也可以是第三方程序、後臺帳戶(Daemon Account)或其他類似事物。它僅僅意味著“當前跟軟體互動的東西”。但考慮到大多數目的和用途,你可以把它認為是Shiro的“使用者”概念。Subject代表了當前使用者的安全操作,SecurityManager則管理所有使用者的安全操作。
  SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通過SecurityManager來管理內部元件例項,並通過它來提供安全管理的各種服務。
  Realm: Realm充當了Shiro與應用安全資料間的“橋樑”或者“聯結器”。也就是說,當對使用者執行認證(登入)和授權(訪問控制)驗證時,Shiro會從應用配置的Realm中查詢使用者及其許可權資訊。
  從這個意義上講,Realm實質上是一個安全相關的DAO:它封裝了資料來源的連線細節,並在需要時將相關資料提供給Shiro。當配置Shiro時,你必須至少指定一個Realm,用於認證和(或)授權
開始之前可以先了解本系列的其他文章,上一篇文章地址:

https://blog.csdn.net/caiqing116/article/details/84581171
1.首先我們插入一個管理員賬號作為登陸測試資料,執行上一篇文章的測試用例,或者手動插入一條資料皆可

@Test
public void testInsert() {
	BasicUser basicUser = new BasicUser();
	basicUser.setId(1);
	basicUser.setUtype(2);
	basicUser.setUserid(UuidUtil.getUuid());
	basicUser.setUsername("墨傾池");
	basicUser.setRealname("墨傾池");//注意 補充了真實姓名的插入
	basicUser.setPassword(EncryptKit.MD5("123456"));
	basicUser.setAge(18);
	int result = basicUserService.insert(basicUser);
	log.info("basicUser:"+basicUser);
	log.info("插入行數:"+result);
}
或者手動插入:INSERT INTO tb_basic_user(userId,utype,username,password,realname,age)
VALUES("3b006cc66a174d668b5cd0ea83eedd0d",1,"admin","E10ADC3949BA59ABBE56E057F20F883E","管理員",18);

2.Maven引入shiro需要的jar包

	<dependency>
		<groupId>org.apache.shiro</groupId>
		<artifactId>shiro-core</artifactId>
		<version>1.2.3</version>
	</dependency>
	<dependency>
		<groupId>org.apache.shiro</groupId>
		<artifactId>shiro-web</artifactId>
		<version>1.2.3</version>
	</dependency>
	<dependency>
		<groupId>org.apache.shiro</groupId>
		<artifactId>shiro-spring</artifactId>
		<version>1.2.3</version>
	</dependency>	

3.配置Spring shiro整合檔案 /spring/applicationContext-shiro.xml,具體功能詳見註釋

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns:context="http://www.springframework.org/schema/context" 
	   xmlns:util="http://www.springframework.org/schema/util"
	   xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
		http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context-4.1.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.1.xsd"
	default-lazy-init="true">
	
	<description>Spring Shiro整合配置檔案</description>

    <!--1. 配置securityManager安全管理器 -->
    <!-- 
    	SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager互動;
		且它管理著所有Subject;可以看出它是Shiro 的核心,它負責與後邊介紹的其他元件進行互動
     -->
 	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="shiroDbRealm" />
    </bean>
    
    <!--2. 配置 CacheManager. 2.1需要加入 ehcache 的 jar 包及配置檔案. -->
    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"> </bean> 
	
	<!--3.配置realm 自定義的Realm-->
    <!-- 
    	Shiro 從從Realm獲取安全資料(如使用者、角色、許可權),就是說SecurityManager要驗證使用者身份,
    	那麼它需要從Realm獲取相應的使用者進行比較以確定使用者身份是否合法;
		也需要從Realm得到使用者相應的角色/許可權進行驗證使用者是否能進行操作;
		可以把Realm看成DataSource , 即安全資料來源 
	-->
    <bean id="shiroDbRealm" class="com.ssm.security.ShiroRealm"></bean>
    
    <!--4.配置lifecycleBeanPostProcessor,可以自動呼叫spring ioc 容器中的shiro bean 的生命週期方法  -->
    <!-- 開啟Shiro註解的Spring配置方式的beans。在lifecycleBeanPostProcessor之後執行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
    
    <!--5. 啟用 IOC 容器中使用 shiro 的註解. 但必須在配置了 LifecycleBeanPostProcessor 之後才可以使用. -->     
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor">
          <property name="proxyTargetClass" value="true" />
    </bean>
    
    <!-- Shiro Filter id值和web.xml檔案配置的過濾器名稱相同 -->
    <bean id="shiroFilter"
        class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <!-- 登入頁面 -->
        <property name="loginUrl" value="/login.jsp" />
        <!-- 登入成功頁面 -->
        <property name="successUrl" value="/WEB-INF/views/home.jsp"/>
        <!-- 沒有許可權的頁面 -->
        <!-- <property name="unauthorizedUrl" value="/unauthorized.jsp"/> -->
      	<!--  
        	配置哪些頁面需要受保護. 
        	以及訪問這些頁面需要的許可權. 
        	1). anon 可以被匿名訪問
        	2). authc 必須認證(即登入)後才可能訪問的頁面. 
        	3). logout 登出.
        	4). roles 角色過濾器
        -->
        <property name="filterChainDefinitions">
            <value>
            	<!-- 登入可匿名訪問 -->
            	/static/**= anon
            	/ssm/shirologin/** = anon
            	/ssm/logout = logout
                <!-- 其他的需要授權訪問authc -->
                /** = authc
            </value>
        </property>
    </bean>

  	<!-- 開啟Shiro註解的Spring配置方式的beans。在lifecycleBeanPostProcessor之後執行 -->
    <bean
        class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager" />
    </bean>
    
    <!-- shiro為整合spring -->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
            	<!-- 無許可權跳轉到登陸頁,可自行定義 -->
                <prop key="org.apache.shiro.authz.UnauthorizedException">/ssm/home</prop>
            </props>
        </property>
    </bean>
</beans>

4.實現Shiro Realm
建立包security建立類ShiroRealm 繼承 AuthorizingRealm,具體實現如下

package com.ssm.security;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import com.ssm.entity.BasicUser;
import com.ssm.exception.AccountException;
import com.ssm.mapper.BasicUserMapper;

public class ShiroRealm extends AuthorizingRealm{
	
	@Autowired
	private BasicUserMapper basicUserMapper;

	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		return null;
	}

	/**
	 * 授權認證
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
		String username = usernamePasswordToken.getUsername();
		String password = String.valueOf(usernamePasswordToken.getPassword());
		BasicUser basicUser = basicUserMapper.selectByUsername(username);
		if(basicUser == null) {
			throw new AccountException("賬號或密碼錯誤");
		}
		if(!password.equals(basicUser.getPassword())){
			throw new AccountException("賬號或密碼錯誤");
		}
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
				basicUser.getUsername(), basicUser.getPassword(), basicUser.getRealname());
		return simpleAuthenticationInfo;
	}

}

建立一個包Exception 建立類AccountException繼承AuthenticationException

package com.ssm.exception;

import org.apache.shiro.authc.AuthenticationException;

public class AccountException extends AuthenticationException{

	private static final long serialVersionUID = 6423461337343398987L;
	
	public AccountException(String msg) {
		super(msg);
	}

}

5.在web.xml檔案中配置ShiroFilter

<filter>  
    <filter-name>shiroFilter</filter-name>  
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>  
    <init-param>  
        <param-name>targetFilterLifecycle</param-name>  
        <param-value>true</param-value>  
    </init-param>  
</filter>  
<filter-mapping>  
    <filter-name>shiroFilter</filter-name>  
    <url-pattern>/*</url-pattern>  
</filter-mapping>

6.實現登陸登出功能security/LoginHandler.java,具體實現如下
執行登陸操作的時候,如果處於未登陸狀態執行currentUser.login(usernamePasswordToken);跳轉到ShiroRealm的doGetAuthenticationInfo方法執行授權認證

package com.ssm.security;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.ssm.exception.AccountException;
import com.ssm.util.EncryptKit;
import com.ssm.util.ResultModel;

@Controller
@RequestMapping("ssm")
public class LoginHandler {

	@RequestMapping("/shirologin")
	@ResponseBody
	public ResultModel shirologin(String username, String password) {
		
		try {
			Subject currentUser = SecurityUtils.getSubject();
			//未認證登入
			if(!currentUser.isAuthenticated()) {
				//密碼進行MD5加密
				UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, EncryptKit.MD5(password));
				//認證登陸
				currentUser.login(usernamePasswordToken);
			}
		} catch (AuthenticationException e) {
			if(e instanceof AccountException) {
				return new ResultModel(1, "賬號或密碼錯誤");
			}
		}
		return new ResultModel(0, "登陸成功");
	}
	
	/**
	 * 登出
	 * @return
	 */
	@RequestMapping("/shirologout")
	public String shirologout() {
		try {
			Subject subject = SecurityUtils.getSubject();
			if(subject.isAuthenticated()) {
				subject.logout();
				//登出成功
				return "redirect:/login.jsp";
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "redirect:/ssm/home";
	}
}

7.建立主頁控制器Controller/HomeController

package com.ssm.controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
 * 主頁控制器
 * @author https://blog.csdn.net/caiqing116
 */
@Controller
@RequestMapping("ssm")
public class HomeController {

	@RequestMapping("/home")
	public String home() {
		return "/home";
	}
}

8.建立登陸頁面login.jsp
頁面中的樣式,js等都有對應的靜態資源,這裡就不一一貼可進入專案git地址下載:
https://github.com/gitcaiqing/mybatis_generator_zh.git,也可以自己簡單建立一個form表單頁即可測試之

 <%@page import="org.apache.shiro.SecurityUtils"%>
<%@page import="org.apache.shiro.subject.Subject"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ include file="/WEB-INF/common/taglib.jsp"%>
<%
	//如果登陸成功,則直接跳轉到主頁
	Subject subject = SecurityUtils.getSubject();
	if(subject.isAuthenticated()){
		response.sendRedirect(request.getContextPath()+"/ssm/home");
	}
%>
<html>
<head>
<title>登陸</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="keywords" content="" />
<script type="application/x-javascript"> addEventListener("load", function() { setTimeout(hideURLbar, 0); }, false); function hideURLbar(){ window.scrollTo(0,1); } </script>
<!-- Bootstrap Core CSS -->
<link href="${base }/static/css/bootstrap.min.css" rel='stylesheet' type='text/css' />
<!-- Custom CSS -->
<link href="${base }/static/css/style.css" rel='stylesheet' type='text/css' />
<link rel="stylesheet" href="${base }/static/css/morris.css" type="text/css"/>
<!-- Graph CSS -->
<link href="${base }/static/css/font-awesome.css" rel="stylesheet">
<link rel="stylesheet" href="${base }/static/css/jquery-ui.css"> 
<!-- jQuery -->
<script src="${base }/static/js/jquery-2.1.4.min.js"></script>
<!-- //jQuery -->
<link href='http://fonts.googleapis.com/css?family=Roboto:700,500,300,100italic,100,400' rel='stylesheet' type='text/css'/>
<link href='http://fonts.googleapis.com/css?family=Montserrat:400,700' rel='stylesheet' type='text/css'>
<!-- lined-icons -->
<link rel="stylesheet" href="${base }/static/css/icon-font.min.css" type='text/css' />
<!-- //lined-icons -->
</head> 
<body>
	<div class="main-wthree">
		<div class="container">
			<div class="sin-w3-agile">
				<h2>Sign In</h2>
				<%-- <form action="${base }/login" method="post"> --%>
				<form id="form" action="#" method="post">
					<div class="username">
						<span class="username">賬號:</span>
						<input type="text" name="username" class="name" placeholder="" required="">
						<div class="clearfix"></div>
					</div>
					<div class="password-agileits">
						<span class="username">密碼:</span>
						<input type="password" name="password" class="password" placeholder="" required="">
						<div class="clearfix"></div>
					</div>
					<div class="rem-for-agile">
						<input type="checkbox" name="remember" class="remember">記住我<br>
						<!-- <a href="#">忘記密碼</a><br> -->
					</div>
					<div class="login-w3">
						<!-- <input type="submit" class="login" value="登陸"> -->
						<input type="button" class="login" value="登陸" onclick="login()">
					</div>
					<div class="clearfix"></div>
				</form>
				<div class="back">
					<a href="https://blog.csdn.net/caiqing116" target="_blank">去主人部落格</a>
				</div>
				<div class="footer">
					<p>&copy; 2018 Design by <a href="https://blog.csdn.net/caiqing116" target="_blank">https://blog.csdn.net/caiqing116</a></p>
				</div>
			</div>
		</div>
	</div>
</body>
<script type="text/javascript">
function login(){
	$.post("${base}/ssm/shirologin",$("#form").serialize(),function(data){
		if(data.resultCode == 0){
			window.location.href = "${base}/ssm/home";
		}else{
			alert(data.msg);
		}
	})
}
</script>
</html>

9建立登陸成功頁views/home.jsp

   <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <%@ include file="/WEB-INF/common/taglib.jsp"%>
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
    	<h1>登陸成功</h1>
    	<h2>
    		<input type="button" value="退出" onclick='window.location.href="${base}/ssm/shirologout"'>
    	</h2> 
    </body>
    </html>

10附MD5加密工具類和ResultModel

package com.ssm.util;

import java.security.MessageDigest;

public class EncryptKit {
	
	private static String MD5 = "MD5";
	private static String SHA = "SHA-1";

	/**
	 * MD5加密
	 * @param string
	 * @return
	 */
	public static String MD5(String string) {
		char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
		try {
			byte[] older = string.getBytes();
			MessageDigest md5 = MessageDigest.getInstance(MD5);
			md5.update(older);
			byte[] newer = md5.digest();
			int j = newer.length;
			char[] chars = new char[j * 2];
			int k = 0;
			for (int i = 0; i < j; i++) {
				byte b = newer[i];
				chars[k++] = hexDigits[b >>> 4 & 0xf];
				chars[k++] = hexDigits[b & 0xf];
			}
			return new String(chars);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * SHA-1加密
	 * @param info
	 * @return
	 */
	public static String SHA(String info) {
		try {
			MessageDigest md = MessageDigest.getInstance(SHA);
			md.update(info.getBytes());
			byte[] digest = md.digest();

			StringBuffer hexstr = new StringBuffer();
			String shaHex = "";
			for (int i = 0; i < digest.length; i++) {
				shaHex = Integer.toHexString(digest[i] & 0xFF);
				if (shaHex.length() < 2) {
					hexstr.append(0);
				}
				hexstr.append(shaHex);
			}
			return hexstr.toString();
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}
}

ResultModle.java

package com.ssm.util;
/**
 * 返回結果物件
 * @author https://blog.csdn.net/caiqing116
 */
public class ResultModel {

	//返回值 0成功 1失敗
	private Integer resultCode;
	//返回的資料
	private Object data;
	//返回的資訊
	private String msg;
	public ResultModel(Integer resultCode, String msg) {
		super();
		this.resultCode = resultCode;
		this.msg = msg;
	}
	public ResultModel(Integer resultCode, Object data, String msg) {
		super();
		this.resultCode = resultCode;
		this.data = data;
		this.msg = msg;
	}
	public Integer getResultCode() {
		return resultCode;
	}
	public void setResultCode(Integer resultCode) {
		this.resultCode = resultCode;
	}
	public Object getData() {
		return data;
	}
	public void setData(Object data) {
		this.data = data;
	}
	public String getMsg() {
		return msg;
	}
	public void setMsg(String msg) {
		this.msg = msg;
	}
	
}