2.3.SSM整合
Shiro 的组件都是 JavaBean/POJO 式的组件,所以非常容易使用 Spring 进行组件管理,可以非常方便的从 ini 配置迁移到 Spring 进行管理,且支持 JavaSE 应用及 Web 应用的集成。
2.3.1.pom文件
4.0.0
net.wanho
shiroTeaching
war
1.0-SNAPSHOT
shiroTeaching Maven Webapp
http://maven.apache.org
UTF-8
4.2.4.RELEASE
3.0.6
1.9.10
1.2.2
1.2.2
1.2
2.5
2.0
junit
junit
4.12
test
org.slf4j
slf4j-log4j12
1.7.2
org.apache.shiro
shiro-web
${shiro.version}
org.apache.shiro
shiro-ehcache
${shiro.version}
org.apache.shiro
shiro-spring
${shiro.version}
org.apache.shiro
shiro-quartz
${shiro.version}
commons-logging
commons-logging
1.1.3
mysql
mysql-connector-java
5.1.18
com.alibaba
druid
1.0.11
org.mybatis
mybatis
${mybatis.version}
org.mybatis
mybatis-spring
${mybatis.spring.version}
org.springframework
spring-context
${spring.version}
org.springframework
spring-beans
${spring.version}
org.springframework
spring-webmvc
${spring.version}
org.springframework
spring-jdbc
${spring.version}
org.springframework
spring-aspects
${spring.version}
org.springframework
spring-jms
${spring.version}
org.springframework
spring-context-support
${spring.version}
jstl
jstl
${jstl.version}
javax.servlet
servlet-api
${servlet-api.version}
provided
javax.servlet
jsp-api
${jsp-api.version}
provided
org.codehaus.jackson
jackson-mapper-asl
${jackson.version}
shiroTeaching
org.apache.tomcat.maven
tomcat7-maven-plugin
/
8080
2.3.2.spring配置文件
applicationContext-shiro
/login/toLogin = anon
/login/checkLogin = anon
/login/logout = authc
/** = user
/error
-
shiroFilter:此处使用 ShiroFilterFactoryBean 来创建 ShiroFilter 过滤器;filters 属性用于定义自己的过滤器,即 ini 配置中的 [filters] 部分;filterChainDefinitions 用于声明 url 和 filter 的关系,即 ini 配置中的 [urls] 部分。
applicationContext-dao
applicationContext-trans
2.3.3.springMVC配置文件
springMVC.xml
web.xml
smePlantform
index.html
index.htm
index.jsp
default.html
default.htm
default.jsp
contextConfigLocation
classpath:spring/applicationContext-*.xml
org.springframework.web.context.ContextLoaderListener
characterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
utf-8
characterEncodingFilter
/*
springServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring/springmvc.xml
1
springServlet
/
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
targetFilterLifecycle
true
shiroFilter
/*
2.3.4 实现shiro整合ehcache
2.4.密码比对,MD5&盐值加密
2.4.1.散列算法
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个 salt(盐),比如加密密码 “admin”,产生的散列值是 “21232f297a57a5a743894a0e4a801fc3”,可以到一些 md5 解密网站很容易的通过散列值得到密码 “admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和 ID(即盐);这样散列的对象是 “密码 + 用户名 +ID”,这样生成的散列值相对来说更难破解。
String str = "hello";
String salt = "123";
String md5 = new Md5Hash(str, salt).toString();//还可以转换为 toBase64()/toHex()
Shiro 还提供了通用的散列支持:
String str = "hello";
String salt = "123";
//内部使用MessageDigest
String simpleHash = new SimpleHash("MD5", str, salt).toString();
通过调用 SimpleHash 时指定散列算法,其内部使用了 Java 的 MessageDigest 实现。
2.4.2.HashedCredentialsMatcher 实现密码验证服务
Shiro 提供了 CredentialsMatcher 的散列实现 HashedCredentialsMatcher,和之前的 PasswordMatcher 不同的是,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐,且生成密码散列值的算法需要自己写,因为能提供自己的盐。
1、生成密码散列值
此处我们使用 MD5 算法,“密码 + 盐(用户名 + 随机数)” 的方式生成散列值:
String algorithmName = "md5";
String username = "liu";
String password = "123";
String salt1 = username;
String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex();
int hashIterations = 2;
SimpleHash hash = new SimpleHash(algorithmName, password, salt1 + salt2, hashIterations);
String encodedPassword = hash.toHex()
如果要写用户模块,需要在新增用户 / 重置密码时使用如上算法保存密码,将生成的密码及 salt2 存入数据库(因为我们的散列算法是:md5(md5(密码 +username+salt2)))。
2、功能实现
User
public class TUser {
private Integer tUserId;
private String tUserName;
private String tUserAccount;
private String tUserPassword;
private String tSort;
private String tUserType;
private Integer tParentUserId;
private Integer departmentId;
private String tStatus;
}
public class UserService implements IUserService{
/**
* 获取所有的角色
* @param username
* @return
*/
@Override
public Set findRoles(String username) {
Set set = new HashSet<>();
set.add("admin");
return set;
}
/**
* 获取所有的权限
* @param username
* @return
*/
@Override
public Set findPermissions(String username) {
Set set = new HashSet<>();
set.add("dustin:test");
return set;
}
/**
* 根据用户名获取用户信息
* @param username
* @return
*/
@Override
public TUser findByUsername(String username) {
TUser user = new TUser();
user.settUserName(username);
user.settUserPassword(shiroMD5("123456"));
user.settSort("abcd");
return user;
}
//shiro的密码加密,参数hashIterations表示加密次数,应该跟配置文件中的相同
private String shiroMD5(String credentials){
//加密方式
String hashAlgorithmName = "MD5";
//String credentials = "123456";
//盐
ByteSource credentialsSalt = ByteSource.Util.bytes("dustin"+"abcd");
//加密次数
int hashIterations = 2;
Object obj = new SimpleHash(hashAlgorithmName, credentials, credentialsSalt, hashIterations);
return obj.toString();
}
}
public class UserRealm extends AuthorizingRealm {
@Resource
private IUserService userService;
/**
* 获取角色与权限
*doGetAuthorizationInfo执行时机有三个,如下:
* 1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
* 2、@RequiresRoles("admin") :在方法上加注解的时候;
* 3、@shiro.hasPermission name = "admin"/@shiro.hasPermission:"dustin:test"在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//获取用户所有角色
authorizationInfo.setRoles(userService.findRoles(username));
//获取用户所有权限
authorizationInfo.setStringPermissions(userService.findPermissions(username));
return authorizationInfo;
}
/**
* 登录信息验证
*
* 1.doGetAuthenticationInfo执行时机如下
* 当调用Subject currentUser = SecurityUtils.getSubject();
* currentUser.login(token);
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
TUser user = userService.findByUsername(username);
if(user == null) {
throw new UnknownAccountException();//没找到帐号
}
if(Boolean.TRUE.equals(user.gettStatus())) {
throw new LockedAccountException(); //帐号锁定
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.gettUserName(), //用户名
user.gettUserPassword(), //密码
ByteSource.Util.bytes(user.gettUserName()+user.gettSort()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
public class loginController {
@RequestMapping("/toLogin")
public String tologin(HttpServletRequest request, HttpServletResponse response, Model model){
System.out.println("来自IP[" + request.getRemoteHost() + "]的访问");
return "login";
}
/**
* 验证用户名和密码
* @return
*/
@RequestMapping("/checkLogin")
public String login(String username,String password,ModelMap map) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject currentUser = SecurityUtils.getSubject();
try {
//使用shiro来验证
if (!currentUser.isAuthenticated()){
//这里是记住我,记cookie
token.setRememberMe(false);
currentUser.login(token);//验证角色和权限
}
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
map.put("code","登录失败:"+"账号密码不正确");
}
return JSONUtils.toJSONString(map);
}
@RequestMapping("/isLogin")
@ResponseBody
public String isLogin() {
Subject currentUser = SecurityUtils.getSubject();
currentUser.isAuthenticated();
return String.valueOf(currentUser.isAuthenticated());
}
@RequestMapping("/logout")
@ResponseBody
public String logout() {
Subject currentUser = SecurityUtils.getSubject();
currentUser.logout();
return "logout success";
}
}
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
登录页面
<%@include file="/taglib.jsp" %>
此处就是把步骤 1 中生成的相应数据组装为 SimpleAuthenticationInfo,通过 SimpleAuthenticationInfo 的 credentialsSalt 设置盐,HashedCredentialsMatcher 会自动识别这个盐。
- saltStyle 表示使用密码 + 盐的机制,authenticationQuery 第一列是密码,第二列是盐;
- 通过 authenticationQuery 指定密码及盐查询 SQL;
此处还要注意 Shiro 默认使用了 apache commons BeanUtils,默认是不进行 Enum 类型转型的,此时需要自己注册一个 Enum 转换器 “BeanUtilsBean.getInstance().getConvertUtils().register(new EnumConverter(), JdbcRealm.SaltStyle.class);” 具体请参考示例 “com.github.zhangkaitao.shiro.chapter5.hash.PasswordTest” 中的代码。
3、spring配置
此处最需要注意的就是 HashedCredentialsMatcher 的算法需要和生成密码时的算法一样。另外 HashedCredentialsMatcher 会自动根据 AuthenticationInfo 的类型是否是 SaltedAuthenticationInfo 来获取 credentialsSalt 盐。
2.4.3.密码重试次数限制
如在 1 个小时内密码最多重试 5 次,如果尝试次数超过 5 次就锁定 1 小时,1 小时后可再次重试,如果还是重试失败,可以锁定如 1 天,以此类推,防止密码被暴力破解。我们通过继承 HashedCredentialsMatcher,且使用 Ehcache 记录重试次数和超时时间。
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String)token.getPrincipal();
//retry count + 1
Element element = passwordRetryCache.get(username);
if(element == null) {
element = new Element(username , new AtomicInteger(0));
passwordRetryCache.put(element);
}
AtomicInteger retryCount = (AtomicInteger)element.getObjectValue();
if(retryCount.incrementAndGet() > 5) {
//if retry count > 5 throw
throw new ExcessiveAttemptsException();
}
boolean matches = super.doCredentialsMatch(token, info);
if(matches) {
//clear retry count
passwordRetryCache.remove(username);
}
return matches;
}
如上代码逻辑比较简单,即如果密码输入正确清除 cache 中的记录;否则 cache 中的重试次数 +1,如果超出 5 次那么抛出异常表示超出重试次数了。
2.5.标签&权限注解
2.5.1.jsp标签
Shiro 提供了 JSTL 标签用于在 JSP/GSP 页面进行权限控制,如根据登录用户显示相应的页面按钮
导入标签库
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
标签库定义在 shiro-web.jar 包下的 META-INF/shiro.tld 中定义。
2.5.1.1.guest 标签
欢迎游客访问,登录
用户没有身份验证时显示相应信息,即游客访问信息。
2.5.1.2.user 标签
已认证通过的用户,setRememberMe = true
用户已经身份验证 / 记住我登录后显示相应的信息。
2.5.1.3.authenticated 标签
用户[ ]已身份验证通过
用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的。
2.5.1.4.notAuthenticated 标签
未身份验证(包括记住我)
用户已经身份验证通过,即没有调用 Subject.login 进行登录,包括记住我自动登录的也属于未进行身份验证。
2.5.1.5.principal 标签
<shiro: principal/>
显示用户身份信息,默认调用 Subject.getPrincipal() 获取,即 Primary Principal。
<shiro:principal type="java.lang.String"/>
相当于 Subject.getPrincipals().oneByType(String.class)。
<shiro:principal property="username"/>
相当于 ((User)Subject.getPrincipals()).getUsername()。
2.5.1.6.hasRole 标签
用户[ ]拥有角色admin
如果当前 Subject 有角色将显示 body 体内容。
2.5.1.7.hasAnyRoles 标签
用户[ ]拥有角色admin或user
;
如果当前 Subject 有任意一个角色(或的关系)将显示 body 体内容。
2.5.1.8.hasPermission 标签
用户[ ]拥有权限user:create
如果当前 Subject 有权限将显示 body 体内容。
2.5.1.8.lacksPermission 标签
用户[ ]没有权限org:create
如果当前 Subject 没有权限将显示 body 体内容。
2.5.2.权限注解
Shiro 提供了相应的注解用于权限控制,如果使用这些注解就需要使用 AOP 的功能来进行判断,如 Spring AOP;Shiro 提供了 Spring AOP 集成用于权限注解的解析和验证。
@RequiresAuthentication
表示当前 Subject 已经通过 login 进行了身份验证;即 Subject.isAuthenticated() 返回 true。
@RequiresUser
表示当前 Subject 已经身份验证或者通过记住我登录的。
@RequiresGuest
表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。
@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
表示当前 Subject 需要角色 admin 和 user。
@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)
表示当前 Subject 需要权限 user:a 或 user:b。
此处使用了 Spring MVC 来测试 Shiro 注解,当然 Shiro 注解不仅仅可以在 web 环境使用,在独立的 JavaSE 中也是可以用的,此处只是以 web 为例了。
在 spring-mvc.xml 配置文件添加 Shiro Spring AOP 权限注解的支持:
如上配置用于开启 Shiro Spring AOP 权限注解的支持;<aop:config proxy-target-class="true">
表示代理类。
接着就可以在相应的控制器(Controller)中使用如下方式进行注解:
@RequiresRoles("admin")
@RequestMapping("/hello2")
public String hello2() {
return "success";
}
访问 hello2 方法的前提是当前用户有 admin 角色。
2.6.会话管理(了解)
Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如 web 容器 tomcat),不管 JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储 / 持久化、容器无关的集群、失效 / 过期支持、对 Web 的透明支持、SSO 单点登录的支持等特性。即直接使用 Shiro 的会话管理可以直接替换如 Web 容器的会话管理。
2.6.1.会话
所谓会话,即用户访问应用时保持的连接关系,在多次交互中应用能够识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后,网站可以记住用户,且在退出之前都可以识别当前用户是谁。
Shiro 的会话支持不仅可以在普通的 JavaSE 应用中使用,也可以在 JavaEE 应用中使用,如 web 应用。且使用方式是一致的。
login("classpath:shiro.ini", "zhang", "123");
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession()
登录成功后使用 Subject.getSession() 即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建一个;另外 Subject.getSession(false),如果当前没有创建 Session 则返回 null(不过默认情况下如果启用会话存储功能的话在创建 Subject 时会主动创建一个 Session)。
session.getId();
获取当前会话的唯一标识。
session.getHost();
获取当前 Subject 的主机地址,该地址是通过 HostAuthenticationToken.getHost() 提供的。
session.getTimeout();
session.setTimeout(毫秒);
获取 / 设置当前 Session 的过期时间;如果不设置默认是会话管理器的全局过期时间。
session.getStartTimestamp();
session.getLastAccessTime();
获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间;如果是 Web 应用,每次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。
session.touch();
session.stop();
更新会话最后访问时间及销毁会话;当 Subject.logout() 时会自动调用 stop 方法来销毁会话。如果在 web 中,调用 javax.servlet.http.HttpSession. invalidate() 也会自动调用 Shiro Session.stop 方法进行销毁 Shiro 的会话。
session.setAttribute("key", "123");
session.removeAttribute("key");
设置 / 获取 / 删除会话属性;在整个会话范围内都可以对这些属性进行操作。
Shiro 提供的会话可以用于 JavaSE/JavaEE 环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。
测试
/**
* 测试会话session
*/
@Test
public void testSession() {
Subject subject = login("classpath:shiro-realm.ini", "zhang", "123");
Session session = subject.getSession();
//获取当前会话的唯一标识
System.out.println(session.getId());
//获取当前 Subject 的主机地址
System.out.println(session.getHost());
//获取 / 设置当前 Session 的过期时间
System.out.println(session.getTimeout());
// 获取会话的启动时间及最后访问时间
System.out.println(session.getStartTimestamp());
System.out.println(session.getLastAccessTime());
// 更新会话最后访问时间及销毁会话
//session.touch();
//session.stop();
//设置属性值
session.setAttribute("name", "zhangsan");
System.out.println(session.getAttribute("name"));
}
2.6.2.会话管理器
会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是 Shiro 的核心组件,顶层组件 SecurityManager 直接继承了 SessionManager,且提供了SessionsSecurityManager 实现直接把会话管理委托给相应的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了 SessionsSecurityManager。
SecurityManager 提供了如下接口:
Session start(SessionContext context); //启动会话
Session getSession(SessionKey key) throws SessionException; //根据会话Key获取会话
另外用于 Web 环境的 WebSessionManager 又提供了如下接口:
boolean isServletContainerSessions();// 是否使用 Servlet 容器的会话
Shiro 还提供了 ValidatingSessionManager 用于验资并过期会话:
void validateSessions();// 验证所有会话是否过期
DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web 环境,其直接使用 Servlet 容器的会话;
DefaultWebSessionManager:用于 Web 环境的实现,可以替代 ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。
spring中配置
2.6.3.会话监听器
会话监听器用于监听会话创建、过期及停止事件:
public class MySessionListener1 implements SessionListener {
@Override
public void onStart(Session session) {//会话创建时触发
System.out.println("会话创建:" + session.getId());
}
@Override
public void onExpiration(Session session) {//会话过期时触发
System.out.println("会话过期:" + session.getId());
}
@Override
public void onStop(Session session) {//退出/会话过期时触发
System.out.println("会话停止:" + session.getId());
}
}
如果只想监听某一个事件,可以继承 SessionListenerAdapter 实现:
public class MySessionListener2 extends SessionListenerAdapter {
@Override
public void onStart(Session session) {
System.out.println("会话创建:" + session.getId());
}
}
2.6.4.会话存储 / 持久化(sessionDAO)
Shiro 提供 SessionDAO 用于会话的 CRUD,即 DAO(Data Access Object)模式实现:
//如DefaultSessionManager在创建完session后会调用该方法;如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;返回会话ID;主要此处返回的ID.equals(session.getId());
Serializable create(Session session);
//根据会话ID获取会话
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
void update(Session session) throws UnknownSessionException;
//删除会话;当会话过期/会话停止(如用户退出时)会调用
void delete(Session session);
//获取当前所有活跃用户,如果用户量多此方法影响性能
Collection getActiveSessions();
Shiro 内嵌了如下 SessionDAO 实现:
AbstractSessionDAO 提供了 SessionDAO 的基础实现,如生成会话 ID 等;CachingSessionDAO 提供了对开发者透明的会话缓存的功能,只需要设置相应的 CacheManager 即可;MemorySessionDAO 直接在内存中进行会话维护;而 EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。
Shiro 提供了使用 Ehcache 进行会话存储,Ehcache 可以配合 TerraCotta 实现容器无关的分布式集群。
首先在 pom.xml 里添加如下依赖:
org.apache.shiro
shiro-ehcache
1.2.2
配置 ehcache.xml
Cache 的名字默认为 shiro-activeSessionCache,即设置的 sessionDAO 的 activeSessionsCacheName 属性值。
spring中配置
- sessionDAO. activeSessionsCacheName:设置 Session 缓存名字,默认就是 shiro-activeSessionCache;
- cacheManager:缓存管理器,用于管理缓存的,此处使用 Ehcache 实现;
- cacheManager.cacheManagerConfigFile:设置 ehcache 缓存的配置文件;
- securityManager.cacheManager:设置 SecurityManager 的 cacheManager,会自动设置实现了 CacheManagerAware 接口的相应对象,如 SessionDAO 的 cacheManager;
如果自定义实现 SessionDAO,继承 CachingSessionDAO 即可:
public class MySessionDAO extends CachingSessionDAO {
private JdbcTemplate jdbcTemplate = JdbcTemplateUtils.jdbcTemplate();
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
String sql = "insert into sessions(id, session) values(?,?)";
jdbcTemplate.update(sql, sessionId, SerializableUtils.serialize(session));
return session.getId();
}
protected void doUpdate(Session session) {
if(session instanceof ValidatingSession && !((ValidatingSession)session).isValid()) {
return; //如果会话过期/停止 没必要再更新了
}
String sql = "update sessions set session=? where id=?";
jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId());
}
protected void doDelete(Session session) {
String sql = "delete from sessions where id=?";
jdbcTemplate.update(sql, session.getId());
}
protected Session doReadSession(Serializable sessionId) {
String sql = "select session from sessions where id=?";
List sessionStrList = jdbcTemplate.queryForList(sql, String.class, sessionId);
if(sessionStrList.size() == 0) return null;
return SerializableUtils.deserialize(sessionStrList.get(0));
}
}
doCreate/doUpdate/doDelete/doReadSession 分别代表创建 / 修改 / 删除 / 读取会话;此处通过把会话序列化后存储到数据库实现。
2.7.实现rememberMe
Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
- 首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会把 RememberMe 的 Cookie 写到客户端并保存下来;
- 关闭浏览器再重新打开;会发现浏览器还是记住你的;
- 访问一般的网页服务器端还是知道你是谁,且能正常访问;
- 但是比如我们访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。
2.7.1.RememberMe 配置
spring中配置
- sessionIdCookie:maxAge=-1 表示浏览器关闭时失效此 Cookie;
- rememberMeCookie:即记住我的 Cookie,保存时长 30 天;
- rememberMe 管理器,cipherKey 是加密 rememberMe Cookie 的密钥;默认 AES 算法;
-
web过滤器
/login/toLogin = anon /login/checkLogin = anon /login/logout = authc /** = user
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数
roles(角色):例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
perms(权限):例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString
是你访问的url里的?后面的参数。
authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证
ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
user:例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查
2.8.shiro异常处理
处理shiro异常有3种方式:
(1)使用Spring-MVC提供的SimpleMappingExceptionResolver;
(2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器;
(3)使用@ExceptionHandler注解实现异常处理;
2.8.1.使用Spring-MVC提供的SimpleMappingExceptionResolver;
代码若抛出授权未通过异常即UnauthorizedException,跳转noPermission.jsp页面(会经过视图解析器后跳转)
/noPermission
注:/noPermission 是指经过视图解析器的页面,这里直接配需要跳转到的页面的名字即可
2.8.2.实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器;
实现HandlerExceptionResolver 接口自定义异常处理器,HandlerExceptionResolver是一个接口,只有一个方法,我们只需要实现这个接口;
我们在springMVC的配置文件中进行如下配置
MyExceptionResolver
public class MyExceptionResolver implements HandlerExceptionResolver {
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
System.out.println("==============异常开始=============");
ex.printStackTrace();
System.out.println("==============异常结束=============");
ModelAndView mv = new ModelAndView("noPermission");
return mv;
}
}
2.8.3.@ExceptionHandler注解实现异常处理
将@ExceptionHandler标注在Controller的方法上,该方法将处理由@RequestMapping方法抛出的异常
首先要增加BaseController类,并在类中使用@ExceptionHandler注解声明异常处理,代码如下:
public class BaseController {
/** 基于@ExceptionHandler异常处理 */
@ExceptionHandler
public String exp(HttpServletRequest request, Exception ex) {
// 根据不同错误转向不同页面
if(ex instanceof org.apache.shiro.authz.UnauthorizedException) {
return "noPermission";
}else {
return "error";
}
}
将需要异常处理的Controller继承BaseController即可