前言: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.youkuaiyun.com/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.youkuaiyun.com/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.youkuaiyun.com/caiqing116" target="_blank">去主人博客</a>
</div>
<div class="footer">
<p>© 2018 Design by <a href="https://blog.youkuaiyun.com/caiqing116" target="_blank">https://blog.youkuaiyun.com/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.youkuaiyun.com/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;
}
}