按照网上的教程,砌了个 shiro 和 springmvc 4 的框架,见 https://gitee.com/luobenyu/admin-console 。主要是琢磨 shiro 登录时的自定义加密、解密流程。
这个集成考虑数据库表、验证流程的可定制性,故不采取 jdbcRealm 的集成方式。查阅了一些非官方的教程,都会提到要覆盖 org.apache.shiro.realm.AuthorizingRealm 类的 doGetAuthenticationInfo(AuthenticationToken arg0) 方法。结合下文,判断为通过自定义 realm 类根据 token 包含的账号信息,查询储存在数据库中的用户登录信息、并包装在 doGetAuthenticationInfo(AuthenticationToken arg0) 返回的 AuthenticationInfo 对象中。该 AuthenticationInfo 对象将提供给 HashedCredentialsMatcher 类的 doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) 方法进行校验。
关于登录时,页面表单传入的密码是否进行加密再进入 login 流程,我查看了 shiro2 的源代码。在 HashedCredentialsMatcher 类找到了这个方法:
/**
* Hashes the provided credentials a total of {@code hashIterations} times, using the given salt. The hash
* implementation/algorithm used is based on the {@link #getHashAlgorithmName() hashAlgorithmName} property.
*
* @param credentials the submitted authentication token's credentials to hash
* @param salt the value to salt the hash, or {@code null} if a salt will not be used.
* @param hashIterations the number of times to hash the credentials. At least one hash will always occur though,
* even if this argument is 0 or negative.
* @return the hashed value of the provided credentials, according to the specified salt and hash iterations.
*/
protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
String hashAlgorithmName = assertHashAlgorithmName();
return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
}
还有这个方法:
/**
* Hash the provided {@code token}'s credentials using the salt stored with the account if the
* {@code info} instance is an {@code instanceof} {@link SaltedAuthenticationInfo SaltedAuthenticationInfo} (see
* the class-level JavaDoc for why this is the preferred approach).
* <p/>
* If the {@code info} instance is <em>not</em>
* an {@code instanceof} {@code SaltedAuthenticationInfo}, the logic will fall back to Shiro 1.0
* backwards-compatible logic: it will first check to see {@link #isHashSalted() isHashSalted} and if so, will try
* to acquire the salt from {@link #getSalt(AuthenticationToken) getSalt(AuthenticationToken)}. See the class-level
* JavaDoc for why this is not recommended. This 'fallback' logic exists only for backwards-compatibility.
* {@code Realm}s should be updated as soon as possible to return {@code SaltedAuthenticationInfo} instances
* if account credentials salting is enabled (highly recommended for password-based systems).
*
* @param token the submitted authentication token from which its credentials will be hashed
* @param info the stored account data, potentially used to acquire a salt
* @return the token credentials hash
* @since 1.1
*/
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
Object salt = null;
if (info instanceof SaltedAuthenticationInfo) {
salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
} else {
//retain 1.0 backwards compatibility:
if (isHashSalted()) {
salt = getSalt(token);
}
}
return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());
}
所以我的判断是:1.在项目 xml 文件中配置了 credentialsMatcher 为官方提供的 HashedCredentialsMatcher ,并在 xml 文件指定了加密算法、加密次数之后;2.在数据库对应的自定义列,写进按照 shiro2 提供的加密步骤一样产生的加密后密码(需要自行实现查询数据库的逻辑);3.在springmvc 的登录(路径)方法中、调用 subject.login(token) 方法时,token 是前端表单直接传来的明文账号、明文密码,因为会在 shiro2 的默认实现中与数据库(后端)提供的 info 进行比对处理。晚一点开始验证。
拖拉快半个月,终于继续验证了。首先,shiro 是基于 filter 起作用的,所以shiro 的页面配置基本和 web.xml 无关。如果你的 web.xml 有配置 welcome-file-list 的,请将它们去除。参照网上一些做法,我们将 loginUrl 配置的地址,在 shiro 的 filterChainDefinitions 中设置为 authc (如果你的 loginUrl 和处理登录信息后重定向的地址不一样,也请把用于重定向的地址设置为 authc)。其他需要登录后才进行操作的地址(如各种业务界面地址),请在 filterChainDefinitions 中配置为 user。至于 css 样式、js脚本或者其他未登录也需要访问的资源,请在 filterChainDefinitions 中设置为 anon 。代码如下:
<!-- also shiro's filter in application context -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/index/loginPage"/>
<property name="unauthorizedUrl" value="/unauthorized"/>
<property name="filterChainDefinitions">
<value>
# 'authc' as login to continue
# 'user' as a valid user or simply returns 404 results
# 'anon' as free to visit anyhow
/styles/** = anon
/index/loginPage = authc
/index/loginSuccess = authc
/doLogout/ = logout
/** = user
</value>
</property>
<property name="filters">
<map>
<entry key="authc" value-ref="formAuthenticationFilter"></entry>
</map>
</property>
</bean>
<bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
<!-- 表单中账号的input名称 -->
<property name="usernameParam" value="usr" />
<!-- 表单中密码的input名称 -->
<property name="passwordParam" value="pwd" />
<!-- 记住我input的名称 -->
<property name="rememberMeParam" value="rememberMe1"/>
<!-- add luoby 2019-08-16 -->
<property name="loginUrl" value="/index/loginPage"/>
<!-- loginUrl may be different from urls that process login info -->
<!-- urls that process login info MUST return the identical address of successUrl if configured -->
<!-- add luoby 2019-08-18 -->
<property name="successUrl" value="/index/hola"/>
</bean>
关于登录后总是回到 loginUrl 的问题, shiro 默认的行为是:登录成功后,回到提交登录前访问的地址。如果你打开浏览器、首先访问了 loginUrl 的地址,然后提交登录,就会像狗追着自己尾巴一样打转了(没错,我就转了一周)。目前我们的测试步骤是:1.先访问一个业务地址(先在 filterChainDefinitions 中设置为 user 了)2.观察是否会重定向到 loginUrl 的地址 3.然后输入用户信息进行登录 4.验证是否回到了我们期望的地址。验证的控制器代码如下:
package com.maple.admin.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("/index")
public class DefaultController {
/**
* show login page
* @param request
* @param response
* @return
*/
@RequestMapping("/loginPage")
@ResponseBody
public ModelAndView myShowLoginPage(HttpServletRequest request, HttpServletResponse response){
ModelAndView mv = new ModelAndView();
mv.setViewName("/login");
return mv;
}
/**
* process login info
* @param request
* @param response
* @return
*/
@RequestMapping(name="/loginSuccess")
@ResponseBody
public ModelAndView myProcessLoginInfo(HttpServletRequest request, HttpServletResponse response){
ModelAndView mv = new ModelAndView();
mv.setViewName("/hola");
String exceptionClassName = (String)request.getAttribute("shiroLoginFailure");
String error = null;
if(UnknownAccountException.class.getName().equals(exceptionClassName)) {
error = "未知账户";
} else if(IncorrectCredentialsException.class.getName().equals(exceptionClassName)) {
error = "用户名/密码错误";
} else if(exceptionClassName != null) {
error = "其他错误:" + exceptionClassName;
}
if (error != null) {
mv.addObject("errorMsg", error);
}
return mv;
}
/**
* @param request
* @param response
* @return
*/
@RequestMapping("/json")
@ResponseBody
public ModelAndView myJsonOutput(HttpServletRequest request, HttpServletResponse response){
String usr = request.getParameter("usr");
String pwd = request.getParameter("pwd");
ModelAndView mv = new ModelAndView();
mv.setViewName("/hola"); // view
if (usr == null || pwd == null) {
mv.addObject("errorMsg", "json output here."); // model
}
return mv;
}
/**
* test static resopurces mapping
* @param request
* @param response
* @return
*/
@RequestMapping("/starter")
@ResponseBody
@RequiresPermissions("user:view")
public ModelAndView myStarter(HttpServletRequest request, HttpServletResponse response){
ModelAndView mv = new ModelAndView();
mv.setViewName("/starter"); // view
return mv;
}
}
还需要一个登录页面、和登录成功后显示信息的页面。登录页面位于 web 目录下的 /WEB-INF/jsp/login.jsp:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>AdminLTE 2 | Log in console</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<!-- Bootstrap 3.3.7 -->
<link rel="stylesheet" href="<c:url value='/styles/bower_components/bootstrap/dist/css/bootstrap.min.css'/>">
<!-- Font Awesome -->
<link rel="stylesheet" href="<c:url value='/styles/bower_components/font-awesome/css/font-awesome.min.css'/>">
<!-- Ionicons -->
<link rel="stylesheet" href="<c:url value='/styles/bower_components/Ionicons/css/ionicons.min.css'/>">
<!-- Theme style -->
<link rel="stylesheet" href="<c:url value='/styles/dist/css/AdminLTE.min.css'/>">
<!-- iCheck -->
<link rel="stylesheet" href="<c:url value='/styles/plugins/iCheck/square/blue.css'/>">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<!-- Google Font -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
</head>
<body class="hold-transition login-page">
<div class="login-box">
<div class="login-logo">
<a href="../../index2.html"><b>Admin</b>LTE</a>
</div>
<!-- /.login-logo -->
<div class="login-box-body">
<p class="login-box-msg">Sign in to start your session</p>
<form action="<c:url value='/index/login'/>" method="post">
<!-- <div class="form-group has-feedback">
<input type="email" class="form-control" placeholder="Email">
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div> -->
<!-- add luoby 2019-08-12 -->
<div class="form-group has-feedback">
<input type="text" class="form-control" placeholder="User" name="usr">
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div>
<!-- end -->
<div class="form-group has-feedback">
<input type="password" class="form-control" placeholder="Password" name="pwd">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-8">
<div class="checkbox icheck">
<label>
<input type="checkbox" name="rememberMe1"> Remember Me
</label>
</div>
</div>
<!-- /.col -->
<div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">Sign In</button>
</div>
<!-- /.col -->
</div>
</form>
<div class="social-auth-links text-center">
<p>- OR -</p>
<a href="#" class="btn btn-block btn-social btn-facebook btn-flat"><i class="fa fa-facebook"></i> Sign in using
Facebook</a>
<a href="#" class="btn btn-block btn-social btn-google btn-flat"><i class="fa fa-google-plus"></i> Sign in using
Google+</a>
</div>
<!-- /.social-auth-links -->
<a href="#">I forgot my password</a><br>
<a href="register.html" class="text-center">Register a new membership</a>
</div>
<!-- /.login-box-body -->
</div>
<!-- /.login-box -->
<!-- jQuery 3 -->
<script src="<c:url value='/styles/bower_components/jquery/dist/jquery.min.js'/>"></script>
<!-- Bootstrap 3.3.7 -->
<script src="<c:url value='/styles/bower_components/bootstrap/dist/js/bootstrap.min.js'/>"></script>
<!-- iCheck -->
<script src="<c:url value='/styles/plugins/iCheck/icheck.min.js'/>"></script>
<script>
$(function () {
$('input').iCheck({
checkboxClass: 'icheckbox_square-blue',
radioClass: 'iradio_square-blue',
increaseArea: '20%' /* optional */
});
});
</script>
</body>
</html>
登录成功后的信息页面就相对简单了,位于 web 目录的 /WEB-INF/jsp/hola.jsp,代码如下:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>
</head>
<body>
Hola! ${errorMsg}
</body>
</html>