二、大致内容
- 实现用户登录和登录状态持久化,以及权限验证
三、效果
首现,通过运用shiro框架并设置好Fliter过滤器,并且设置好过滤规则,可以将所有请求都拦截,并且在配置之后,shiro框架会接管Tomcat的会话管理。因为HTTP请求是没有状态的,因此Tomcat自身会产生一个sessionID来对每个会话进行标记,而shiro框架对这一过程进行了接管,并能按自己的配置设置这个sessionID在cookie中的字段名。然后使用缓存,例如ehcache,redis等,进行这个sessionID的存储。我这里将其命名为sid
。
shiro框架中,请求经过过滤器后,会交给SecurityManager进行处理,而SecurityManager一般又从Realm中获取验证数据,所以我要结合我的数据接口的情况来自定义一个Realm,而它一般要实现两个功能,身份验证和权限授予,分别在doGetAuthorizationInfo()
和doGetAuthenticationInfo()
中进行,返回的是AuthorizationInfo对象和AuthenticationInfo对象。这两个分别表示着授权信息和验证信息。在我的代码中,对于前者,我通过setRoles()
函数向其中添加了和当前用户有关的角色信息,并且通过setStringPermissions()
函数向其中添加了和当前用户有关的权限信息,然后返回。而对于后者,在构建authenticationInfo
的时候,使用了四个参数,分别是登录者发送的用户名和密码,还有随机生成的盐值,以及这个realm的类名。
再到用户验证,那么要匹配登录者发送的信息和数据库中对应的信息,就会有个验证和匹配的地方,这个匹配的借口我在RetryLimitHashedCredentialsMatcher
类中实现,它继承了HashedCredentialsMatcher
这个类,这里的匹配和验证本质上是交给父类去做的,而我增加的内容,则是从缓存中取出现在登录的这个用户名,它的连续错误登录次数,若是过多,则会抛出ExcessiveAttemptsException
。
这些以上的内容,都会在配置文件spring-shiro.xml
中用到
大概的顺序级和关系大概如下:
然后再到我的登录入口,我经常会用到Subject
对象,这个对象存有用户相关的各种信息,例如用户名,sessionID等,登录和登出操作也是以它为入口进行的。而Subject
对象为什么能存储呢,通过观看源码可以发现,它从SecurityUtils
中返回,而这又是从ThreadContext.getSubject()
中获取的,再接着看,创建Subject的时候,是将其和ThreadContext
绑定的,相当于与线程绑定了。
然后演示一下
首先进行登录,试试输错的情况:
然后成功登录,并在返回信息中加入用户相关的信息,如昵称,性别,头像url等:
然后可以看看cookie中的sid:
然后进行信息的请求,这个API是需要用户登录的,因此登录了的用户可以成功通过:
然后删除sid,此刻已经相当于未登录状态,即一个新的会话,然后再次请求刚刚的那个API:
出现错误了,即表明shiro框架的验证和匹配是成功的。
再试试看持久登录,rememberMe的选项:
可以看到,cookie中多了rememberMe的key-value,这个就是用来存储在客户端,能够保持持久登录的字段和密文。试试重启服务器,并且删去sid:
依然是成功获取了,这表明持久化登录的功能是成功了的
四、代码详情和解析
我选择的是shiro框架来实现用户登录和验证的功能,正好附带了权限验证的功能,能够在后续实现用户的锁定,设置超级管理员等等,可以方便App的功能升级。而权限验证也方便了Api的开发。
首先需要改变表的结构,当然,可以结合之前的表,将之前的表改为userinfo,对应的mapper,service,dao和controller也改为userinfoxxx,然后在数据库中添加如下表格:
create table sys_users (
id bigint auto_increment comment '编号',
username varchar(100) comment '用户名',
password varchar(100) comment '密码',
salt varchar(100) comment '盐值',
role_id varchar(50) comment '角色列表',
locked bool default false comment '是否锁定',
constraint pk_sys_users primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_sys_users_username on sys_users(username);
create table sys_roles (
id bigint auto_increment comment '角色编号',
role varchar(100) comment '角色名称',
description varchar(100) comment '角色描述',
pid bigint comment '父节点',
available bool default false comment '是否锁定',
constraint pk_sys_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_sys_roles_role on sys_roles(role);
create table sys_permissions (
id bigint auto_increment comment '编号',
permission varchar(100) comment '权限编号',
description varchar(100) comment '权限描述',
rid bigint comment '此权限关联角色的id',
available bool default false comment '是否锁定',
constraint pk_sys_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_sys_permissions_permission on sys_permissions(permission);
create table sys_users_roles (
id bigint auto_increment comment '编号',
user_id bigint comment '用户编号',
role_id bigint comment '角色编号',
constraint pk_sys_users_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
create table sys_roles_permissions (
id bigint auto_increment comment '编号',
role_id bigint comment '角色编号',
permission_id bigint comment '权限编号',
constraint pk_sys_roles_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
这是实现之后的表:
其中test
和user
是测试用表,和项目整体无关。
然后要实现User
,Role
,Permission
的mapper,dao和service,这里还是比较基础和底层的内容。
然后是Service的实现类:
UserServcieImpl:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private PasswordHelper passwordHelper;
/**
* 添加用户-角色关系
*
* @param userId
* @param roleIds
*/
@Transactional
public void correlationRoles(Long userId, Long... roleIds) {
if (roleIds == null || roleIds.length == 0) {
return;
}
for (Long roleId: roleIds) {
if (!exists(userId, roleId)) {
userDao.correlationRoles(userId, roleId);
}
}
}
/**
* 移除用户-角色关系
*
* @param userId
* @param roleIds
*/
@Transactional
public void uncorrelationRoles(Long userId, Long... roleIds) {
if (roleIds == null || roleIds.length == 0) {
return;
}
for (Long roleId: roleIds) {
if (exists(userId, roleId)) {
userDao.uncorrelationRoles(userId, roleId);
}
}
}
/**
* 判断当前的用户和角色是否存在
*
* @param userId
* @param roleId
* @return
*/
@Transactional
public boolean exists(Long userId, Long roleId) {
return userDao.exists(userId, roleId);
}
@Transactional
@Override
public List<Role> findRoles(String username) {
return userDao.findRoles(username);
}
@Transactional
@Override
public List<Permission> findPermissions(String username) {
return userDao.findPermissions(username);
}
@Transactional
@Override
public void changePassword(Long id, String newPassword) {
//User user = ((UserServiceImpl)AopContext.currentProxy()).findById(id);
User user = userDao.findById(id);
// 因为数据库中`locked`字段使用的类型:`tinyint(1)`,
// 那么使用mybatis查询数据库会自动将数据转换成boolean类型(使用了boolean类型接收),0:false;1或其他非零数字:true
System.out.println("是否锁定:" + user.getLocked());
if (user==null){
return;
}
user.setPassword(newPassword);
passwordHelper.encryptPassword(user);
userDao.update(user);
}
@Transactional
@Override
public void create(User user) {
//加密密码
passwordHelper.encryptPassword(user);
userDao.create(user);
}
@Transactional
@Override
public void delete(Long id) {
userDao.delete(id);
}
@Transactional
@Override
public void update(User user) {
//加密密码
passwordHelper.encryptPassword(user);
userDao.update(user);
}
@Transactional
@Override
public List<User> findAll() {
return userDao.findAll();
}
@Transactional
@Override
public User findByName(String username) {
User user = userDao.findByName(username);
if (user == null) {
return null;
}
return user;
}
@Transactional
@Override
public User findById(Long id) {
User user = userDao.findById(id);
// 因为数据库中`locked`字段使用的类型:`tinyint(1)`,
// 那么使用mybatis查询数据库会自动将数据转换成boolean类型(使用了boolean类型接收),0:false;1或其他非零数字:true
System.out.println("是否锁定:" + user.getLocked());
if (user == null) {
return null;
}
return user;
}
@Transactional
@Override
public void deleteAllUserRoles(Long id) {
userDao.deleteAllUserRoles(id);
}
RoleServiceImpl:
@Service
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleDao roleDao;
@Transactional
@Override
public void correlationPermissions(Long roleId, Long... permissionIds) {
if(permissionIds == null || permissionIds.length == 0){
return;
}
for(Long permissionId : permissionIds){
if(!exists(roleId, permissionId)){
roleDao.correlationPermissions(roleId,permissionId);
}
}
}
@Transactional
@Override
public void uncorrelationPermissions(Long roleId, Long... permissionIds) {
if(roleId == null || permissionIds.length == 0){
return;
}
for(Long permissionId : permissionIds){
if(exists(roleId, permissionId)){
roleDao.uncorrelationPermissions(roleId, permissionId);
}
}
}
@Transactional
@Override
public Role findById(Long id) {
return roleDao.findById(id);
}
/**
* 查询表中是否存在此数据
* @param roleId
* @param permissionId
* @return
*/
private boolean exists(Long roleId, Long permissionId) {
return roleDao.exists(roleId, permissionId);
}
@Transactional
@Override
public void create(Role role) {
if (role.getPid() == null){
role.setPid(0L);
}
roleDao.create(role);
}
@Transactional
@Override
public void delete(Long id) {
//先将和角色相关的表删除
roleDao.deleteUserRole(id);
//再删除角色表数据
roleDao.deleteRole(id);
}
@Transactional
@Override
public List<Role> findAll() {
return roleDao.findAll();
}
@Transactional
@Override
public List<Permission> findRolePermissionByRoleId(Long id) {
return roleDao.findRolePermissionByRoleId(id);
}
@Transactional
@Override
public List<Permission> findPermissionByRoleId(Long id) {
return roleDao.findPermissionByRoleId(id);
}
@Transactional
@Override
public void update(Role role) {
roleDao.update(role);
}
@Transactional
@Override
public void deleteAllRolePermissions(Long id) {
roleDao.deleteAllRolePermissions(id);
}
@Transactional
@Override
public void updateUserRole_Id(Role role) {
roleDao.updateUserRole_Id(role);
}
}
以及PermissionServiceImpl:
@Service
public class PermissionServiceImpl implements PermissionService {
@Autowired
private PermissionDao permissionDao;
@Transactional
@Override
public void create(Permission permission) {
permissionDao.create(permission);
}
@Transactional
@Override
public void delete(Long id) {
//先将和Permission相关的表数据删除
permissionDao.deleteRolePermission(id);
//再删除Permission表数据
permissionDao.deletePermission(id);
}
@Transactional
@Override
public List<Permission> findAll() {
return permissionDao.findAll();
}
@Transactional
@Override
public void update(Permission permission) {
permissionDao.update(permission);
}
@Transactional
@Override
public Permission findById(Long id) {
return permissionDao.findById(id);
}
@Transactional
@Override
public List<Role> findRoleByPermissionId(Long id) {
return permissionDao.findRoleByPermissionId(id);
}
@Transactional
@Override
public void deleteAllPermissionsRoles(Long id) {
permissionDao.deleteAllPermissionsRoles(id);
}
@Transactional
@Override
public void correlationRoles(Long permissionId, Long roleId) {
permissionDao.correlationRoles(permissionId, roleId);
}
}
然后在创建用户的时候,要将用户的用户名和密码存入数据库,密码并不明文存储,那么就要通过一个工具类来进行加密:
@Component
public class PasswordHelper {
//实例化RandomNumberGenerator对象,用于生成一个随机数
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
//散列算法名称
private String algorithName = "MD5";
//散列迭代次数
private int hashInterations = 2;
public RandomNumberGenerator getRandomNumberGenerator() {
return randomNumberGenerator;
}
public void setRandomNumberGenerator(RandomNumberGenerator randomNumberGenerator) {
this.randomNumberGenerator = randomNumberGenerator;
}
public String getAlgorithName() {
return algorithName;
}
public void setAlgorithName(String algorithName) {
this.algorithName = algorithName;
}
public int getHashInterations() {
return hashInterations;
}
public void setHashInterations(int hashInterations) {
this.hashInterations = hashInterations;
}
//加密算法
public void encryptPassword(User user){
if (user.getPassword() != null){
//对user对象设置盐:salt;这个盐值是randomNumberGenerator生成的随机数,所以盐值并不需要我们指定
//spring-shiro.xml中,storedCredentialsHexEncoded=true 则需要.toHex()
//spring-shiro.xml中,storedCredentialsHexEncoded=false 则需要.toBase64()
user.setSalt(randomNumberGenerator.nextBytes().toHex());
//调用SimpleHash指定散列算法参数:1、算法名称;2、用户输入的密码;3、盐值(随机生成的);4、迭代次数
//user实体的方法getCredentialsSalt获取的salt'=username+salt,而salt为一串随机序列
String newPassword = new SimpleHash(
algorithName,
user.getPassword(),
ByteSource.Util.bytes(user.getCredentialsSalt()),
hashInterations).toHex();
user.setPassword(newPassword);
}
}
}
然后就要实现shiro框架中最重要的内容了,Realm类:
UserRealm:
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 权限校验
* @param principals
* @return
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("权限校验--执行了doGetAuthorizationInfo...");
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//注意这里的setRoles和setStringPermissions需要的都是一个Set<String>类型参数
Set<String> role = new HashSet<String>();
List<Role> roles = userService.findRoles(username);
for (Role r : roles){
role.add(r.getRole());
}
authorizationInfo.setRoles(role);
Set<String> permission = new HashSet<String>();
List<Permission> permissions = userService.findPermissions(username);
for (Permission p : permissions){
permission.add(p.getPermission());
}
authorizationInfo.setStringPermissions(permission);
return authorizationInfo;
}
/**
* 身份校验
* @param token
* @return
* @throws AuthenticationException
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("身份校验--执行了goGetAuthenticationInfo...");
//token.getCredentials().toString();
//credentials这个属性,在UsernamePasswordToken中其实是个Object,查看源代码,getCredentials()方法返回的就是password
//若要正确得到UsernamePasswordToken的password,可以将credentials转为char[]再String.valof()方法获得String
String username = (String) token.getPrincipal();
User user = userService.findByName(username);
if (user == null) {
throw new UnknownAccountException(); //没有找到账号
}
if (Boolean.TRUE.equals(user.getLocked())) {
throw new LockedAccountException(); //账号锁定
}
//交给AuthenticationRealm使用CredentialsMatcher进行密码匹配
//SimpleAccount则可以直接获取明文
//user实体的方法getCredentialsSalt获取的salt'=username+salt,而salt为一串随机序列
//在PasswordHelper中,同样是用这种方式作为盐值传给SimpleHash来加密
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getUsername(), //用户名
user.getPassword(), //密码
ByteSource.Util.bytes(user.getCredentialsSalt()),
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();
}
}
Authentication的目的是对用户的密码进行校验,Authorization的目的是对用户的权限进行校验,前者主要在登录的过程进行,后者则在用户请求设定了权限要求的API的实现进行。
这里是负责获取数据库的信息和用户传入的信息,真正校验的类,在另一个地方。
RetryLimitHashedCredentialsMatcher:
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
private Cache<String, AtomicInteger> passwordRetryCache;
public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager){
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
//return count+1
AtomicInteger retryCount = passwordRetryCache.get(username);
System.out.println("prove the cache is OK: " + retryCount);
if(retryCount == null){
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username,retryCount);
}
if(retryCount.incrementAndGet() > 5){
throw new ExcessiveAttemptsException();
}
boolean matches = super.doCredentialsMatch(token,info);
if(matches){
//clear retry count
passwordRetryCache.remove(username);
}
return matches;
}
}
这里会从缓存中调取用户登录的信息,看看是否存在多次登录的状况。然后调用父类的方法进行验证。
然后,就要配置了,这方面还是很麻烦的。
首先是ehcache.xml
,这是为了能够让session持久化,以及能够提升效率
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="shirocache">
<diskStore path="java.io.tmpdir"/>
<!-- 登录记录缓存 锁定10分钟 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<!--授权-->
<cache name="authorizationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<!--验证-->
<cache name="authenticationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro-activeSessionCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
然后是log4j.properties
:
#Console
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.out
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
log4j.logger.org.apache=ERROR
log4j.logger.org.mybatis=ERROR
log4j.logger.org.springframework=ERROR
# Default Shiro logging
log4j.logger.org.apache.shiro=ERROR
#这个须要
log4j.logger.log4jdbc.debug=ERROR
log4j.logger.com.gk.mapper=ERROR
log4j.logger.jdbc.audit=ERROR
log4j.logger.jdbc.resultset=ERROR
#这个打印SQL语句非常重要
log4j.logger.jdbc.sqlonly=DEBUG
log4j.logger.jdbc.sqltiming=ERROR
log4j.logger.jdbc.connection=FATAL
最后是最重要的spring-shiro.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<!-- 缓存管理器 使用Ehcache实现 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:other/ehcache.xml"/>
</bean>
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher" class="com.mySSM.credentials.RetryLimitHashedCredentialsMatcher">
<!-- 使用Spring构造器注入cacheManager -->
<constructor-arg ref="cacheManager"/>
<!-- 指定散列算法名称 -->
<property name="hashAlgorithmName" value="md5"/>
<!-- 指定散列迭代的次数 -->
<property name="hashIterations" value="2"/>
<!-- 是否储存散列后的密码为16进制,需要和生成密码时的一样,默认是base64 -->
<property name="storedCredentialsHexEncoded" value="true"/>
</bean>
<!-- Realm实现 -->
<bean id="userRealm" class="com.mySSM.realm.UserRealm">
<!-- 使用credentialsMatcher实现密码验证服务 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<!-- 是否启用缓存 -->
<property name="cachingEnabled" value="true"/>
<!-- 是否启用身份验证缓存 -->
<property name="authenticationCachingEnabled" value="true"/>
<!-- 缓存AuthenticationInfo信息的缓存名称 -->
<property name="authenticationCacheName" value="authenticationCache"/>
<!-- 是否启用授权缓存,缓存AuthorizationInfo信息 -->
<property name="authorizationCachingEnabled" value="true"/>
<!-- 缓存AuthorizationInfo信息的缓存名称 -->
<property name="authorizationCacheName" value="authorizationCache"/>
</bean>
<!-- 会话ID生成器,用于生成会话的ID,使用JavaScript的UUID生成 -->
<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
<!-- 会话Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid"/>
<!-- 如果设置为true,则客户端不会暴露给服务端脚本代码,有助于减少某些类型的跨站脚本攻击 -->
<property name="httpOnly" value="true"/>
<property name="maxAge" value="-1"/><!-- maxAge=-1表示浏览器关闭时失效此Cookie -->
</bean>
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="2592000"/><!-- 30天 -->
</bean>
<!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- cipherKey是加密rememberMe Cookie的密匙,默认AES算法 -->
<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
<property name="cookie" ref="rememberMeCookie"/>
</bean>
<!-- 会话DAO -->
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
<!-- 设置session缓存的名称,默认就是shiro-activeSessionCache -->
<property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
</bean>
<!-- 会话验证调度器 -->
<bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
<property name="sessionValidationInterval" value="1800000"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 设置全局会话过期时间:默认30分钟 -->
<property name="globalSessionTimeout" value="1800000"/>
<!-- 是否自动删除无效会话 -->
<property name="deleteInvalidSessions" value="true"/>
<!-- 会话验证是否启用 -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<!-- 会话验证调度器 -->
<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
<!-- 会话持久化sessionDao -->
<property name="sessionDAO" ref="sessionDAO"/>
<!-- 是否启用sessionIdCookie,默认是启用的 -->
<property name="sessionIdCookieEnabled" value="true"/>
<!-- 会话Cookie -->
<property name="sessionIdCookie" ref="sessionIdCookie"/>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="cacheManager"/>
<!-- 设置securityManager安全管理器的rememberMeManger -->
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean>
<!-- 基于Form表单的身份验证过滤器 -->
<bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
<!-- 这两个字段,username和password要和表单中定义的username和password字段名称相同,可以更改,但是表单和XML要对应 -->
<property name="usernameParam" value="username"/>
<property name="passwordParam" value="password"/>
<property name="loginUrl" value="/login"/>
<!-- rememberMeParam是rememberMe请求参数名,请求参数是boolean类型,true表示记住我 -->
<property name="rememberMeParam" value="rememberMe"/>
</bean>
<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的安全管理器,所有关于安全的操作都会经过SecurityManager -->
<property name="securityManager" ref="securityManager"/>
<!-- 系统认证提交地址,如果用户退出即session丢失就会访问这个页面 -->
<property name="loginUrl" value="/login"/>
<!-- 登录成功后重定向的地址,不建议配置 -->
<!--<property name="successUrl" value="/index.do"/>-->
<!-- 权限验证失败跳转的页面,需要配合Spring的ExceptionHandler异常处理机制使用 -->
<property name="unauthorizedUrl" value="/unauthorized"/>
<property name="filters">
<util:map>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</util:map>
</property>
<!-- 自定义的过滤器链,从上向下执行,一般将`/**`放到最下面 -->
<property name="filterChainDefinitions">
<value>
<!-- 静态资源不拦截 -->
/static/** = anon
/lib/** = anon
/js/** = anon
/resources/** = anon
<!-- 登录页面不拦截 -->
/login.jsp = anon
/login.do = anon
/login = anon
/userinfo/** = anon
/testsid = anon
/testsid.jsp = anon
/testsid.do = anon
/initial = anon
<!-- Shiro提供了退出登录的配置`logout`,会生成路径为`/logout`的请求地址,访问这个地址即会退出当前账户并清空缓存 -->
/logout = logout
<!-- user表示身份通过或通过记住我通过的用户都能访问系统 -->
/index.jsp = user
<!-- authc表示访问该地址用户必须身份验证通过,即Subject.isAuthenticated() == true -->
/authenticated.jsp = authc
<!-- `/**`表示所有请求,表示访问该地址的用户是身份验证通过或RememberMe登录的都可以 -->
/** = user
</value>
</property>
</bean>
<!-- Shiro生命周期处理器-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
</beans>
然后在spring-web.xml
中添加如下内容:
<!-- Shiro提供了相应的注解实现权限控制,但是需要AOP功能的支持
定义AOP切面,用于代理如@RequiresRole注解的控制器,进行权限控制
-->
<aop:config proxy-target-class="true"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
最后在web.xml
中添加过滤器:
<!-- shiro 安全过滤器 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
<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>
<!-- filter的"/*"会拦截,就是说所有的这个项目的请求都会被捕捉,过滤 -->
然后完成对应的controller的编写即可,例如登录:
@Controller
public class LoginController {
/**
* 用户登录的入口
*
* @param username
* @param password
* @param model
* @return
*/
@RequestMapping("/login")
public String login(
@RequestParam(value = "username", required = false) String username,
@RequestParam(value = "password", required = false) String password,
@RequestParam(value = "remember", required = false) String remember,
Model model) {
System.out.println("登陆用户输入的用户名:" + username + ",密码:" + password);
String error = null;
if (username != null && password != null) {
//初始化
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
if (remember != null){
if (remember.equals("on")) {
//说明选择了记住我
token.setRememberMe(true);
} else {
token.setRememberMe(false);
}
}else{
token.setRememberMe(false);
}
try {
//登录,即身份校验,由通过Spring注入的UserRealm会自动校验输入的用户名和密码在数据库中是否有对应的值
subject.login(token);
System.out.println("用户是否登录:" + subject.isAuthenticated());
return "redirect:index.do";
} catch (UnknownAccountException e) {
e.printStackTrace();
error = "用户账户不存在,错误信息:" + e.getMessage();
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
error = "用户名或密码错误,错误信息:" + e.getMessage();
} catch (LockedAccountException e) {
e.printStackTrace();
error = "该账号已锁定,错误信息:" + e.getMessage();
} catch (DisabledAccountException e) {
e.printStackTrace();
error = "该账号已禁用,错误信息:" + e.getMessage();
} catch (ExcessiveAttemptsException e) {
e.printStackTrace();
error = "该账号登录失败次数过多,错误信息:" + e.getMessage();
} catch (Exception e){
e.printStackTrace();
error = "未知错误,错误信息:" + e.getMessage();
}
} else {
error = "请输入用户名和密码";
}
//登录失败,跳转到login页面
model.addAttribute("error", error);
return "login";
}
// /**
// * 退出登录,我们不需要实现,Shiro的Filter过滤器会帮我们生成一个logout请求,
// * 当浏览器访问`/logout`请求时,Shiro会自动清空缓存并重定向到配置好的loginUrl页面
// *
// * @return
// */
// @RequestMapping("/logout")
// public String logout() {
// Subject subject = SecurityUtils.getSubject();
// subject.logout();
// return "index";
// }
/**
* 登录成功,跳转到首页
*
* @return
*/
@RequestMapping("/index")
public String index() {
return "index";
}
四、遇到的问题
1.首先在配置完,启动的时候,报错了,报错信息是Error creating bean with name 'shiroFilter': Requested bean is currently in creation: Is there an unresolvable circular reference?
,然后差了一下,发现各种可能性都有,最后发现,我之前为了实现在service中能够调用自身的方法,<aop:aspectj-autoproxy expose-proxy="true"/>
,而在配置shiro的时候,又用了
<aop:config proxy-target-class="true"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
因此混用产生了冲突,去掉之前那个就好,然后改了一下service。
2.接着是sessionID的问题,我用的服务器是Tomcat,会自动产生一个sessionID并存入cookie中, 然而shiro配置了之后也会产生一个sessionID,我定义的名字是sid
,而tomcat产生的叫JSESSIONID
,然后我在controller中打印HttpSession和Subject.GetSession的id,发现是一样的,意味着JSESSIONID应该是没有用的,也即shiro接管了sessionid的产生等一系列过程。那么要如何去除tomcat自己产生的sessionid呢,在jsp文件中加入session="false"
即可,运行了一次之后,发现往后所有连接的cookie中都不带有JSESSIONID
了。
五、感想
这次因为量有点大,并且bug出的多。不过收获还是很足的,用户登录功能可以说是基础,后续只要在这上面添砖加瓦就可以了。