Shiro框架实现登录验证和授权,以及客户端持久化登录

请注意,本文编写于 822 天前,最后修改于 556 天前,其中某些信息可能已经过时。

二、大致内容

  • 实现用户登录和登录状态持久化,以及权限验证

三、效果

首现,通过运用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中用到

大概的顺序级和关系大概如下:

未命名文件-2
未命名文件-2

然后再到我的登录入口,我经常会用到Subject对象,这个对象存有用户相关的各种信息,例如用户名,sessionID等,登录和登出操作也是以它为入口进行的。而Subject对象为什么能存储呢,通过观看源码可以发现,它从SecurityUtils中返回,而这又是从ThreadContext.getSubject()中获取的,再接着看,创建Subject的时候,是将其和ThreadContext绑定的,相当于与线程绑定了。

然后演示一下

首先进行登录,试试输错的情况:

屏幕快照 2019-06-23 23.47.36.png
屏幕快照 2019-06-23 23.47.36.png

然后成功登录,并在返回信息中加入用户相关的信息,如昵称,性别,头像url等:

屏幕快照 2019-06-23 23.54.47.png
屏幕快照 2019-06-23 23.54.47.png

然后可以看看cookie中的sid:

屏幕快照 2019-06-23 23.48.44.png
屏幕快照 2019-06-23 23.48.44.png

然后进行信息的请求,这个API是需要用户登录的,因此登录了的用户可以成功通过:

屏幕快照 2019-06-23 23.52.57.png
屏幕快照 2019-06-23 23.52.57.png

然后删除sid,此刻已经相当于未登录状态,即一个新的会话,然后再次请求刚刚的那个API:

屏幕快照 2019-06-23 23.53.18.png
屏幕快照 2019-06-23 23.53.18.png

出现错误了,即表明shiro框架的验证和匹配是成功的。

再试试看持久登录,rememberMe的选项:

屏幕快照 2019-06-24 00.17.33.png
屏幕快照 2019-06-24 00.17.33.png

可以看到,cookie中多了rememberMe的key-value,这个就是用来存储在客户端,能够保持持久登录的字段和密文。试试重启服务器,并且删去sid:

屏幕快照 2019-06-24 00.20.17.png
屏幕快照 2019-06-24 00.20.17.png

依然是成功获取了,这表明持久化登录的功能是成功了的


四、代码详情和解析

我选择的是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;

这是实现之后的表:

屏幕快照 2019-06-23 22.32.32
屏幕快照 2019-06-23 22.32.32

其中testuser是测试用表,和项目整体无关。

然后要实现UserRolePermission的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。

参考:Spring AOP 不同配置方式产生的冲突问题

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出的多。不过收获还是很足的,用户登录功能可以说是基础,后续只要在这上面添砖加瓦就可以了。

此处评论已关闭