本系列博文目录:https://my.oschina.net/u/3452433/blog/907396
本文所提供的项目实例,是我将公司项目中的shiro代码进行了抽取、整理并添加了一些注释而形成的。
所以例子中并不包含shiro所有的功能,但是本系列文章前9篇所讲解的内容在这里都是可以找到的。
本示例项目所使用的技术如下:
集成开发环境为IDEA,项目构建使用spring boot,包管理使用maven,页面展示使用freemaker,控制层使用spring mvc等。
在本篇博文中会贴出主要代码,完整的项目已经上传到码云大家可以下载查看使用。
项目码云地址:http://git.oschina.net/imlichao/shiro-example
项目结构
freemaker配置文件
package pub.lichao.shiro.config;
import com.jagregory.shiro.freemarker.ShiroTags;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import java.util.HashMap;
import java.util.Map;
/**
* FreeMarker配置文件
*/
@Configuration
public class FreemarkerConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer(FreeMarkerProperties freeMarkerProperties) {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPaths(freeMarkerProperties.getTemplateLoaderPath()); //模板加载路径默认 "classpath:/templates/"
configurer.setDefaultEncoding("utf-8");//设置页面默认编码(不设置页面中文乱码)
Map<String,Object> variables=new HashMap<String,Object>();
variables.put("shiro", new ShiroTags());
configurer.setFreemarkerVariables(variables);//添加shiro自定义标签
return configurer;
}
}
shiro配置文件
package pub.lichao.shiro.config;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;
import pub.lichao.shiro.shiro.AuthenticationFilter;
import pub.lichao.shiro.shiro.ShiroRealm;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro配置
*/
@Configuration
public class ShiroConfig {
/**
* 创建EhCache缓存类
* @return
*/
@Bean(name = "shiroCacheManager")
public EhCacheManager shiroCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml");//指定缓存配置文件路径
return ehCacheManager;
}
/**
* 创建安全认证资源类
* (自己实现的登陆和授权认证规则)
*/
@Bean(name = "shiroRealm")
public ShiroRealm shiroRealm(EhCacheManager shiroCacheManager) {
ShiroRealm realm = new ShiroRealm();
realm.setCacheManager(shiroCacheManager); //为资源类配置缓存
return realm;
}
/**
* 创建保存记住我信息的Cookie
*/
@Bean(name = "rememberMeCookie")
public SimpleCookie getSimpleCookie() {
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("rememberMe");//cookie名字
simpleCookie.setHttpOnly(true); //设置cookieHttpOnly,保证cookie安全
simpleCookie.setMaxAge(604800); //保存7天 单位秒
return simpleCookie;
}
/**
* 创建记住我管理器
*/
@Bean(name = "rememberMeManager")
public CookieRememberMeManager getCookieRememberMeManager(SimpleCookie rememberMeCookie) {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
byte[] cipherKey = Base64.decode("wGiHplamyXlVB11UXWol8g==");//创建cookie秘钥
cookieRememberMeManager.setCipherKey(cipherKey); //存入cookie秘钥
cookieRememberMeManager.setCookie(rememberMeCookie); //存入记住我Cookie
return cookieRememberMeManager;
}
/**
* 创建默认的安全管理类
* 整个安全认证流程的管理都由此类负责
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm,EhCacheManager shiroCacheManager,CookieRememberMeManager rememberMeManager) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); //创建安全管理类
defaultWebSecurityManager.setRealm(shiroRealm); //指定资源类
defaultWebSecurityManager.setCacheManager(shiroCacheManager);//为管理类配置Session缓存
defaultWebSecurityManager.setRememberMeManager(rememberMeManager);//配置记住我cookie管理类
return defaultWebSecurityManager;
}
/**
* 获得拦截器工厂类
*/
@Bean (name = "authenticationFilter")
public AuthenticationFilter authenticationFilter() {
return new AuthenticationFilter();
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager,AuthenticationFilter authenticationFilter) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);//设置SecurityManager,必输
shiroFilterFactoryBean.setLoginUrl("/login");//配置登录路径(登录页的路径和表单提交的路径必须是同一个,页面的GET方式,表单的POST方式)
shiroFilterFactoryBean.setSuccessUrl("/home");//配置登录成功页路径
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");//配置没有权限跳转的页面
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/", "anon"); //无需登录认证和授权就可访问的路径使用anon拦截器
filterChainDefinitionMap.put("/home/**", "user");//需要登录认证的路径使用authc或user拦截器
filterChainDefinitionMap.put("/user/**", "user,perms[user-jurisdiction]");//需要权限授权的路径使用perms拦截器
filterChainDefinitionMap.put("/admin/**", "user,perms[admin-jurisdiction]");//authc和perms拦截器可同时使用
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);//设置拦截规则
Map<String, Filter> map = new HashMap<String, Filter>();
map.put("authc", authenticationFilter);//自定义拦截器覆盖了FormAuthenticationFilter登录拦截器所用的拦截器名authc
shiroFilterFactoryBean.setFilters(map);//添加自定义拦截器
return shiroFilterFactoryBean;
}
/**
* 注册shiro拦截器
*/
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter")); //创建代理拦截器,并指定代理shiro拦截器
filterRegistration.addInitParameter("targetFilterLifecycle", "true");//设置拦截器生命周期管理规则。false(默认)由SpringApplicationContext管理,true由ServletContainer管理。
filterRegistration.setEnabled(true);// 激活注册拦截器
filterRegistration.addUrlPatterns("/*");//添加拦截路径
return filterRegistration;
}
}
主页Controller层
package pub.lichao.shiro.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* Controller - 主页
*/
@Controller
public class HomeController {
/**
* 进入主页
* @param request
* @param redirectAttributes
* @return
*/
@RequestMapping(value = "/home", method = RequestMethod.GET)
public String home(HttpServletRequest request, RedirectAttributes redirectAttributes) {
return "home";
}
/**
* 进入无权限提示页
* @param request
* @param redirectAttributes
* @return
*/
@RequestMapping(value = "/unauthorized", method = RequestMethod.GET)
public String unauthorized(HttpServletRequest request, RedirectAttributes redirectAttributes) {
return "unauthorized";
}
/**
* 进入admin页(用户无此权限)
*/
@RequestMapping(value = "/admin", method = RequestMethod.GET)
public String admin(HttpServletRequest request, RedirectAttributes redirectAttributes) {
return "admin";
}
}
登录页Controller层
package pub.lichao.shiro.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* Controller - 登陆
*/
@Controller
public class LoginController {
/**
* 根路径重定向到登录页
* @return
*/
@RequestMapping(value = "/", method = RequestMethod.GET)
public String loginForm() {
return "redirect:login";
}
/**
* 进入登录页面
* @return
*/
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String loginInput(@ModelAttribute("message") String message) {
if (message != null && !message.equals("")){
//此处演示一下重定向到此方法时通过addFlashAttribute添加的参数怎么获取
System.out.println("addFlashAttribute添加的参数 :"+ message);
}
//判断是否已经登录 或 是否已经记住我
if (SecurityUtils.getSubject().isAuthenticated() || SecurityUtils.getSubject().isRemembered()) {
return "redirect:/home";
} else {
return "login";
}
}
/**
* 登录表单提交
* @param request
* @param redirectAttributes
* @return
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(HttpServletRequest request, RedirectAttributes redirectAttributes) {
//如果认证未通过获得异常并重定向到登录页
String message = null;
String loginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);//取得登陆失败异常
if (loginFailure.equals("pub.lichao.shiro.shiro.CaptchaAuthenticationException")) {
message = "验证码错误";//自定义登陆认证异常 - 用于验证码错误提示
} else if (loginFailure.equals("org.apache.shiro.authc.UnknownAccountException")) {
message = "用户不存在";//未找到账户异常
}else if (loginFailure.equals("org.apache.shiro.authc.IncorrectCredentialsException")) {
message = "密码错误";//凭证(密码)错误异常
} else if (loginFailure.equals("org.apache.shiro.authc.AuthenticationException")) {
message = "账号认证失败";//认证异常
}else{
message = "未知认证错误";//未知认证错误
}
//重定向参数传递,能够将参数传递到最终页面
// (用addAttribute的时候参数会写在url中所以要用addFlashAttribute)
redirectAttributes.addFlashAttribute("message", message);
return "redirect:login";
}
/**
* 退出登录
* @param redirectAttributes
* @return
*/
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout(RedirectAttributes redirectAttributes) {
//调用shiro管理工具类的退出登录方法
SecurityUtils.getSubject().logout();
redirectAttributes.addFlashAttribute("message", "您已安全退出");
return "redirect:login"; //退出后返回到登录页
}
}
自定义拦截器
package pub.lichao.shiro.shiro;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Filter - 自定义登陆拦截器
* 继承并重写默认的登录拦截器
*/
public class AuthenticationFilter extends FormAuthenticationFilter {
/**
* 创建Token
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String username = getUsername(request);//获取用户名 表单name:username
String password = getPassword(request);//获取密码 表单name:password
boolean rememberMe = isRememberMe(request);//获取是否记住我 表单name:rememberMe
String captchaId = WebUtils.getCleanParam(request, "captchaId");//获取验证码id
String captcha = WebUtils.getCleanParam(request, "captcha");//获取用户输入的验证码字符
return new CaptchaAuthenticationToken(username, password,captchaId, captcha, rememberMe);//存入自己定义的包含验证码的Token
}
/**
* 登录验证成功之后
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
return super.onLoginSuccess(token, subject, request, response);
}
/**
* 当访问被拒绝
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//访问被拒绝时默认行为是返回登录页,但是当使用ajax进行登录时要返回403错误
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestType = request.getHeader("X-Requested-With"); //获取http头参数X-Requested-With
if (requestType != null && requestType.equalsIgnoreCase("XMLHttpRequest")) { //头参数X-Requested-With存在并且值为XMLHttpRequest说明是ajax请求
response.sendError(HttpServletResponse.SC_FORBIDDEN); //返回403错误 - 执行访问被禁止
return false;
}else{
return super.onAccessDenied(servletRequest, servletResponse);
}
}
}
自定义认证异常
package pub.lichao.shiro.shiro;
import org.apache.shiro.authc.AuthenticationException;
/**
* 自定义登陆认证异常 - 用于验证码错误提示
*/
public class CaptchaAuthenticationException extends AuthenticationException {
}
自定义令牌
package pub.lichao.shiro.shiro;
import org.apache.shiro.authc.UsernamePasswordToken;
/**
* Token - 自定义登录令牌
* 继承并重写默认的登录令牌
*/
public class CaptchaAuthenticationToken extends UsernamePasswordToken {
/**
* 自定义构造方法
*/
public CaptchaAuthenticationToken(String username, String password, String captchaId, String captcha, boolean rememberMe) {
super(username, password, rememberMe);
this.captcha=captcha;
this.captchaId=captchaId;
}
/**
* 自定义参数
*/
private String captchaId; //验证码id
private String captcha; //录入的验证码字符
public String getCaptchaId() {
return captchaId;
}
public void setCaptchaId(String captchaId) {
this.captchaId = captchaId;
}
public String getCaptcha() { return captcha; }
public void setCaptcha(String captcha) {
this.captcha = captcha;
}
}
身份信息
package pub.lichao.shiro.shiro;
/**
* 身份信息
*/
public class Principal implements java.io.Serializable{
/** 用户ID */
private Long userId;
/** 用户名 */
private String username;
public Principal(Long userId, String username) {
this.userId = userId;
this.username = username;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
认证资源
登录认证和授权逻辑实现都在这里面
/**
* @(#)ShiroRealm.java
* Description:
* Version : 1.0
* Copyright: Copyright (c) 苗方清颜 版权所有
*/
package pub.lichao.shiro.shiro;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.ArrayList;
import java.util.List;
/**
* 安全认证资源类
*/
public class ShiroRealm extends AuthorizingRealm {
/**
* 登录认证(身份验证)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
CaptchaAuthenticationToken authenticationToken = (CaptchaAuthenticationToken) token; //获得登录令牌
String username = authenticationToken.getUsername();
String password = new String(authenticationToken.getPassword());//将char数组转换成String类型
String captchaId = authenticationToken.getCaptchaId();
String captcha = authenticationToken.getCaptcha();
// 验证用户名密码和验证码是否正确
usernamePasswordAndCaptchaAuthentication(username,password,captchaId,captcha);
//创建身份信息类(自定义的)
Principal principal = new Principal(1L, username);
//认证通过返回认证信息类
return new SimpleAuthenticationInfo(principal, password, getName());
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取当前登录用户的信息(登录认证时取得的)
Principal principal = (Principal) principals.getPrimaryPrincipal();
//使用登录信息中存入的userId获取当前用户所有权限
List<String> authorities = getAuthority(principal.getUserId());
//创建授权信息类
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//将权限存入授权信息类
authorizationInfo.addStringPermissions(authorities);
return authorizationInfo;
}
/**
* 验证用户名密码和验证码是否正确
* @param username
* @param password
* @param captchaId
* @param captcha
* @return
*/
private void usernamePasswordAndCaptchaAuthentication(String username,String password,String captchaId,String captcha){
//验证验证码是否正确
if(!captchaId.equals("1") || !captcha.equals("yyyy")){
throw new CaptchaAuthenticationException(); //验证码错误时抛出自定义的 验证码错误异常
}
//验证用户名是否存在
if(!username.equals("admin")){
throw new UnknownAccountException(); //用户并不存在异常
}
//密码加密(SHA256算法)
String salt = "c1bac4173f3df3bf0241432a45ac3922";//密言一般由系统为每个用户随机生成
String sha256 = new Sha256Hash(password, salt).toString(); //使用sha256进行加密密码
//验证密码是否正确
if(!sha256.equals("aa07342954e1ca7170257e74515139cc27710ff703e6fee784d0a4ea1e09f9da")){
throw new IncorrectCredentialsException();//密码错误异常
}
}
/**
* 获取用户权限
* @param userId
* @return
*/
private List<String> getAuthority(Long userId){
List<String> authority = new ArrayList<String>();
if(userId.equals(1L)){
authority.add("user-jurisdiction");
// authority.add("admin-jurisdiction");
}
return authority;
}
}
缓存配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="shiro-ehcache">
<!-- 缓存文件存放目录 -->
<!-- java.io.tmpdir代表操作系统默认的临时文件目录,不同操作系统路径不同 -->
<!-- windows 7 C:\Users\Administrator\AppData\Local\Temp -->
<!-- linux /tmp -->
<diskStore path="${java.io.tmpdir}/shiro/ehcache"/>
<!-- 设置缓存规则-->
<!--
maxElementsInMemory:缓存文件在内存上最大数目
maxElementsOnDisk:缓存文件在磁盘上的最大数目
eternal:缓存是否永不过期。true永不过期,false会过期
timeToIdleSeconds :缓存最大空闲时间,空闲超过此时间则过期(单位:秒)。当eternal为false时有效
timeToLiveSeconds :缓存最大的生存时间,从创建开始超过这个时间则过期(单位:秒)。当eternal为false时有效
overflowToDisk:如果内存中数据超过内存限制,是否缓存到磁盘上
diskPersistent:是否在磁盘上持久化缓存,系统重启后缓存依然存在
-->
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
diskPersistent="false"/>
</ehcache>
view层代码
login
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<title>管理平台</title>
</head>
<body style="text-align:center">
<div style="margin:0 auto;">
<form action="/login" method="post">
<h3>管理平台登录</h3>
<label>用户名:</label>
<input type="text" placeholder="请输入用户名" name="username" id="username" value="admin"/><br><br>
<label>密 码:</label>
<input type="password" placeholder="请输入密码" name="password" id="password" value="111111"/><br><br>
<label>验证码:</label>
<input type="text" placeholder="请输入验证码" name="captcha" id="captcha" value="yyyy"/><br><br>
<input type="hidden" name="captchaId" id="captchaId" value="1"/>
<label>保持登录:</label>
<input type="checkbox" name="rememberMe" id="rememberMe" />(保持7天)<br><br>
<button type="submit">登录</button><br><br>
<#if message??><span style="color: red" >${message}</span></#if>
</form>
</div>
</body>
</html>
home
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<title>管理平台</title>
</head>
<body style="text-align:center">
<h1>欢迎登录管理平台!</h1>
<br><br>
<a href="logout">退出登录</a>
<br><br>
<@shiro.hasPermission name = "user-jurisdiction">
用户拥有user-jurisdiction权限才能看见此内容!
</@shiro.hasPermission>
<br><br>
<button onclick="javascript:window.location.href='admin'">
进入到admin页(需要有权限)
</button>
</body>
</html>
admin
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<title>管理平台</title>
</head>
<body style="text-align:center">
<h1>用户需要有admin-jurisdiction权限才能看到此页内容</h1><br><br>
<a href="javascript:history.go(-1)">返回</a>
</body>
</html>
unauthorized
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<title>管理平台</title>
</head>
<body style="text-align:center">
你没有访问此功能的权限!
<a href="javascript:history.go(-1)">返回</a>
</body>
</html>