Shiro介绍以及各功能的使用

Shiro学习

官方文档

什么是shiro

Apache Shiro 是一个强大而灵活的开源安全框架,可是实现身份验证、授权、企业会话管理和加密。

整体框架

在这里插入图片描述

  • Subject:当前与软件交互的实体(用户、第 3 方服务、cron 作业等)的特定于安全的“视图”。

  • SecurityManager: 是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行拦截并控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理

  • Authenticator:认证器,负责主体认证的,即确定用户是否登录成功,我们可以使用  Shiro 提供的方法来认证,也可以自定义去实现,自己判断什么时候算是用户登录成功**

  • Authrizer:授权器,即权限授权,给Subject 分配权限,以此很好的控制用户可访问的资源

  • RealmShiro 和应用程序安全数据之间的“桥梁”或“连接器”。当需要与安全相关数据(如用户帐户)进行实际交互以执行身份验证(登录)和授权(访问控制)时,Shiro 从一个或多个为应用程序配置的 Realms 中查找其中的许多内容。您可以根据Realms需要配置任意数量(通常每个数据源一个),Shiro 将根据需要与他们协调进行身份验证和授权。

  • SessionManager:为了可以在不同的环境下使用session 功能,shiro 实现了自己的 sessionManager ,可以用在非 web 环境下和分布式环境下使用

    • **SessionDAO:**对 session 的 CURD 操作
  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;

  • Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的*。

实现的功能

Apache Shiro 是一个综合性的应用程序安全框架,具有许多功能。

在这里插入图片描述

Shiro 的目标是 Shiro 开发团队所说的“应用程序安全的四大基石”——身份验证、授权、会话管理和加密:

  • Authentication身份验证:有时也称为“登录”,这是证明用户就是他们所说的身份的行为。
  • Authorization授权:访问控制的过程,即确定“谁”可以访问“什么”。
  • SessionManager会话管理:管理特定于用户的会话,即使是在非 Web 或 EJB 应用程序中。
  • Cryptography加密:使用密码算法确保数据安全,同时仍然易于使用。

还有一些附加功能可以在不同的应用程序环境中支持和加强这些问题,尤其是:

  • Web Support(Web 支持):Shiro 的 Web 支持 API 有助于轻松保护 Web 应用程序。
  • Caching(缓存):缓存是 Apache Shiro 的 API 中的第一层公民,以确保安全操作保持快速和高效。
  • Concurrency(并发):Apache Shiro 通过其并发特性支持多线程应用程序。
  • Testing(测试):测试支持可帮助您编写单元和集成测试,并确保您的代码按预期得到保护。
  • “Run As”:允许用户假设另一个用户的身份(如果他们被允许)的功能,有时在管理方案中很有用。
  • Remember Me(记住我):记住跨会话的用户身份,因此他们只需要在强制时登录。

认证

在这里插入图片描述

探究源码

使用subject.login(token);发起登录请求,可以发现调用了DelegatingSubject的login方法,交由securityManager去调用login方法,成功之后设置相应的权限信息

在这里插入图片描述

下面可以看到DefaultSecurityManager的login方法调用了当前对象authenticate方法,即调用了认证器的authenticate(token)方法,如果成功获取认证信息,则进行验证

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

可以看到点前认证器有调用了doAuthenticate(token)方法

在这里插入图片描述

实际上调用了realm的doSingleRealmAuthentication或者doMultiRealmAuthentication方法

在这里插入图片描述

由于我们当前只创建了一个realm,所以调用doSingleRealmAuthentication,实际调用了realm的认证方法realm.getAuthenticationInfo

在这里插入图片描述

可以看到我们会先从缓存中获取验证信息,如果缓存中没有,再到调用当前realm的doGetAuthenticationInfo方法,然后把验证信息存入缓存,之后还会进行加密判断

在这里插入图片描述

由于我们重写了realm方法,所以是去数据库获取验证数据

在这里插入图片描述

验证判断

当我们取到认证信息后,进行判断

在这里插入图片描述

在这里插入图片描述

这里由于我们重写了凭证匹配器,加了md5加密,所以匹配器会调用我们重写的匹配器,取出密码加密后再与与认证信息匹配。一般情况下不重配匹配器的话,都是直接返回密码与认证信息匹配。

    <bean id="realm" class="com.xc.shiro.CustomReaml">
        <property name="credentialsMatcher" ref="matcher"></property>
    </bean>

    <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher" id="matcher">
        <property name="hashAlgorithmName" value="md5"></property>
        <property name="hashIterations" value="1"></property>
    </bean>

在这里插入图片描述

重写后的匹配器的匹配方法

this.hashProvidedCredentials是根据我们提交的密码加密后算出相应的hash值作为比对信息

this.getCredentials(info)获取我们的认证信息里的认证(存储与数据库中的密码)

在这里插入图片描述

一般情况下的匹配器的匹配方法
this.getCredentials(token)直接获取token里的password作为比对信息
在这里插入图片描述

授权

授权流程类似于认证,都是Security Manager拦截,由subject提交请求,Security Manager调用Authorizer授权器,授权器根据请求身份通过realm判断是否具有权限

简单示例认证和授权

public class Test {

    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

    @Before
    public void addUser(){
        simpleAccountRealm.addAccount("xx","123456","admin");
    }

    @org.junit.Test
    public void testAuthentication(){
        //1.创建SecurityManager
        DefaultSecurityManager manager = new DefaultSecurityManager();
        manager.setRealm(simpleAccountRealm);

        //2.主体提交认证
        SecurityUtils.setSecurityManager(manager);
        Subject subject = SecurityUtils.getSubject();

        UsernamePasswordToken token = new UsernamePasswordToken("xx","123456");
        subject.login(token);

        System.out.println("isAuthenticated:" + subject.isAuthenticated());

        subject.checkRole("admin1");

        subject.logout();
        System.out.println("isAuthenticated:" + subject.isAuthenticated());

    }
}

realm

Shiro内置的Realm:IniRealm和JdbcRealm
IniRealm

使用ini文件配置认证的相关用户,角色以及权限

public class IniRealmTest {

    IniRealm iniRealm = new IniRealm("classpath:user.ini");


    @org.junit.Test
    public void testAuthentication(){
        //1.创建SecurityManager
        DefaultSecurityManager manager = new DefaultSecurityManager();
        manager.setRealm(iniRealm);

        //2.主体提交认证
        SecurityUtils.setSecurityManager(manager);
        Subject subject = SecurityUtils.getSubject();

        UsernamePasswordToken token = new UsernamePasswordToken("xx","123456");
        subject.login(token);

        System.out.println("isAuthenticated:" + subject.isAuthenticated());

        subject.checkRole("admin");
        subject.checkPermission("user:select1");

        subject.logout();
        System.out.println("isAuthenticated:" + subject.isAuthenticated());

    }
}

ini文件

users:xx为用户名,123456为密码,admin为用户角色(可以多配几个用户)

roles:角色=权限

[users]
xx=123456,admin  
[roles]
admin=user:select
JdbcRealm

创建JdbcRealm对象,默认情况下它的数据库查询为下图

在这里插入图片描述

可使用默认的或者自己重写sql语句然后通过set复制给相应的属性,比如:

String sql = "select password from test_users where username = ?";
jdbcRealm.setAuthenticationQuery(sql);

该方法重写了认证查询语句

public class JDBCRealmTest {
    //链接数据库,这里因为只是简单测试直接用代码块链接数据库
    DruidDataSource dataSource = new DruidDataSource();

    {
        dataSource.setUsername("root");
        dataSource.setPassword("密码");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test");
    }


    @org.junit.Test
    public void testAuthentication(){
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(dataSource);
        jdbcRealm.setPermissionsLookupEnabled(true);

        //自定义sql查询语句
/*        String sql = "select password from test_users where username = ?";
        jdbcRealm.setAuthenticationQuery(sql);

        String sql1 = "select role_name from test_user_roles where username = ?";
        jdbcRealm.setUserRolesQuery(sql1);*/

        //1.创建SecurityManager
        DefaultSecurityManager manager = new DefaultSecurityManager();
        manager.setRealm(jdbcRealm);

        //2.主体提交认证
        SecurityUtils.setSecurityManager(manager);
        Subject subject = SecurityUtils.getSubject();

        UsernamePasswordToken token = new UsernamePasswordToken("xxx","123456");
        subject.login(token);

        System.out.println("isAuthenticated:" + subject.isAuthenticated());

        subject.checkRole("user");
//        subject.checkPermission("user:select");


        subject.logout();
        System.out.println("isAuthenticated:" + subject.isAuthenticated());

    }
}
自定义realm

实现AuthorizingRealm,重写认证和授权的方法

使用map模拟数据库

public class CustomReaml extends AuthorizingRealm {
    private Map<String,String> userMap = new HashMap<>();

    {
        userMap.put("xx","1a46be489100d6a089358eff29b98f7a");
        super.setName("customRealm");
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //从数据库或者缓存在获取角色数据
        Set<String> roles = getRolesByUserName(userName);

        Set<String> permissions = getPermissionsByUserName(userName);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setRoles(roles);
        simpleAuthorizationInfo.setStringPermissions(permissions);
        return simpleAuthorizationInfo;
    }

    private Set<String> getPermissionsByUserName(String userName) {
        Set<String> set = new HashSet<>();
        set.add("user:delete");
        set.add("user:select");
        return set;
    }

    /**
     * 模拟用户从数据库中获取权限
     * @param userName
     * @return
     */
    private Set<String> getRolesByUserName(String userName) {
        Set<String> sets = new HashSet<>();
        sets.add("admin");
        sets.add("user");
        return sets;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //从主体传过来的认证信息中,获取用户名
        String userName = (String) authenticationToken.getPrincipal();

        //2.通过用户名到数据库中获取凭证
        String password = getPasswordByUserName(userName);

        if(password != null){
            SimpleAuthenticationInfo authenticationInfo =
                    new SimpleAuthenticationInfo(userName,password,"customRealm");
            //加盐
            authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(userName));
            return authenticationInfo;
        }
        return null;
    }

    /**
     * 模拟数据库查询凭证
     * @param userName
     * @return
     */
    private String getPasswordByUserName(String userName) {
        return userMap.get(userName);
    }

    public static void main(String[] args) {
        Md5Hash md5Hash = new Md5Hash("123456","xx");
        System.out.println(md5Hash);
    }
}

测试

public class CustomRealmTest {

    @org.junit.Test
    public void testAuthentication(){
        CustomReaml customReaml = new CustomReaml();
        //1.创建SecurityManager
        DefaultSecurityManager manager = new DefaultSecurityManager();
        manager.setRealm(customReaml);

        //shiro加密
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("md5"); //使用md5进行加密
        matcher.setHashIterations(1);  //加密一次
        customReaml.setCredentialsMatcher(matcher);

        //2.主体提交认证
        SecurityUtils.setSecurityManager(manager);
        Subject subject = SecurityUtils.getSubject();

        UsernamePasswordToken token = new UsernamePasswordToken("xx","123456");
        subject.login(token);

        System.out.println("isAuthenticated:" + subject.isAuthenticated());

        subject.checkRole("admin");
        subject.checkPermissions("user:delete");

        subject.logout();
        System.out.println("isAuthenticated:" + subject.isAuthenticated());

    }
}

shiro加密

采用自定义realm进行测试

//shiro加密
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("md5");
        matcher.setHashIterations(1);
        customReaml.setCredentialsMatcher(matcher);

附件:上述练习的pom文件

<dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.7.1</version>
        </dependency>


        <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>



    </dependencies>

常见过滤器

        //添加shiro的内置过滤器
        /**
         * anon:无需认证就可以访问
         * authc:必须认证了才能访问
         * user:必须拥有记住我功能才能用
         * perms:拥有对某个资源的权限才能访问
         * role:拥有某个角色权限才能访问
         */

自动可用的默认过滤器实例由DefaultFilter 枚举定义,枚举的name字段是可用于配置的名称。他们是:

Filter NameClass简介示例
anonorg.apache.shiro.web.filter.authc.AnonymousFilter无需认证/user/**=anon
authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter必须认证,无认证跳转到登陆页面/user/**=authc
authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter需要通过httpBasic验证,不通过跳转到登陆页面/user/**=authcBasic
authcBearerorg.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter
invalidRequestorg.apache.shiro.web.filter.InvalidRequestFilter阻止恶意请求的请求过滤器。无效请求将使用 400 响应代码进行响应。如果在请求 URI 中找到以下字符,此过滤器将检查并阻止请求:分号 - 可以通过设置禁用 blockSemicolon = false;反斜杠 - 可以通过设置禁用 blockBackslash = false;非 ASCII 字符 - 可以通过设置blockNonAscii = false禁用,禁用此检查的功能将在未来版本中删除。
logoutorg.apache.shiro.web.filter.authc.LogoutFilter退出拦截器,在收到请求后,将立即注销当前正在执行的操作subject,然后将它们重定向到已配置的redirectUrl[redirectUrl:退出成功后重定向的地址(/)]
noSessionCreationorg.apache.shiro.web.filter.session.NoSessionCreationFilter阻止在请求时创建会话
permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter具有相关权限,当有多个参数时必须每个参数都通过才通过/user/**=perm[“user:select”,“user:add”]
portorg.apache.shiro.web.filter.authz.PortFilter非port端口会转到port端口
restorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilterrest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串/user/**=rest[user],会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll)
rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter角色授权拦截器,验证用户是否拥有所有角色;当有多个参数时必须每个参数都通过才通过/user/**=roles[admin]
sslorg.apache.shiro.web.filter.authz.SslFilter只有请求协议是https才能通过;否则自动跳转会https端口(443)/user/**=ssl
userorg.apache.shiro.web.filter.authc.UserFilter用户拦截器,用户已经身份验证/记住我登录的都可/user/**=user

示例

注意:规则是有顺序的,从上到下,拦截范围必须是从小到大的

如果写在同一行则都要满足,如/index.html = user,port[80](同时满足user和port)

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="login.html"></property>
        <property name="unauthorizedUrl" value="403.html"/>
        <property name="filterChainDefinitions">
            <value>
                /login.html = anon
                /subLogin = anon
<!--                /index.html = user-->
<!--                /testRole = roles["admin"]
                /testRole1 = roles["admin","admin1"]
                /testPerms1 = perms["user:delete","user:add"]
                /testPerms = perms["user:delete"]-->
                /testRole = rolesOr["admin","admin1"]
                /testRole1 = roles["admin","admin1"]
                /* = authc
            </value>
        </property>
        <property name="filters">
            <util:map>
                <entry key="rolesOr" value-ref="roleOrFilter"></entry>
            </util:map>
        </property>
    </bean>

会话管理

重写sessionDao方法,继承AbstractSessionDAO,实现seesion的增删改查操作,交由redis存储

public class RedisSessionDao extends AbstractSessionDAO {

    @Autowired
    private JedisUtil jedisUtil;

    private final String SHIRO_SESSION_PREFIX = "xc_session:"; //前缀

    private byte[] getKey(String key){
        return (SHIRO_SESSION_PREFIX + key).getBytes();
    }

    private void saveSession(Session session){
        if(session != null && session.getId() != null){
            byte[] key = getKey(session.getId().toString());
            byte[] value = SerializationUtils.serialize(session);

            jedisUtil.set(key,value);
            jedisUtil.expire(key,600);
        }
    }
    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        System.out.println("create session");
        assignSessionId(session,sessionId);
//        System.out.println(sessionId + " " + session.getId());
        saveSession(session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        System.out.println("read session");
        if(sessionId == null){
            return null;
        }
        byte[] key = getKey(sessionId.toString());
        byte[] value = jedisUtil.get(key);
        return (Session) SerializationUtils.deserialize(value);
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        saveSession(session);
    }

    @Override
    public void delete(Session session) {
        if(session == null || session.getId() == null){
            return;
        }
        byte[] key = getKey(session.getId().toString());
        jedisUtil.del(key);
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<byte[]> keys = jedisUtil.keys(SHIRO_SESSION_PREFIX);
        Set<Session> sessions = new HashSet<>();
        if(CollectionUtils.isEmpty(keys)){
            return sessions;
        }
        for (byte[] key : keys){
            Session session = (Session) SerializationUtils.deserialize(jedisUtil.get(key));
            sessions.add(session);
        }
        return sessions;
    }
}

redis的增删改查,redis链接在源代码中

@Component
public class JedisUtil {

    @Resource
    private JedisPool jedisPool;

    private Jedis getResource(){
        return jedisPool.getResource();
    }

    public byte[] set(byte[] key, byte[] value) {
        Jedis jedis = getResource();
        try {
            jedis.set(key,value);
            return value;
        } finally {
            jedis.close();
        }
    }

    public void expire(byte[] key, int i) {
        Jedis jedis = getResource();
        try {
            jedis.expire(key,i);
        } finally {
            jedis.close();
        }
    }

    public byte[] get(byte[] key) {
        Jedis jedis = getResource();
        try {
            return jedis.get(key);
        } finally {
            jedis.close();
        }
    }

    public void del(byte[] key) {
        Jedis jedis = getResource();
        try {
            jedis.del(key);
        } finally {
            jedis.close();
        }
    }

    public Set<byte[]> keys(String shiro_session_prefix) {
        Jedis jedis = getResource();
        try {
            return jedis.keys((shiro_session_prefix + "*").getBytes());
        } finally {
            jedis.close();
        }
    }

}

由于每次查询session都需要去redis中查找,这样一来效率有点低,所以直接把session绑定到request中,下次请求可以到request中查询,就不用一直去redis中查找了

重写sessionManager方法,继承DefaultWebSessionManager

public class CustomSessionManager extends DefaultWebSessionManager {
    @Override
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        Serializable sessionId = getSessionId(sessionKey);
        ServletRequest request = null;
        if(sessionKey instanceof WebSessionKey){
            request = ((WebSessionKey) sessionKey).getServletRequest();
        }
        if(request != null && sessionId != null){
            Session session = (Session) request.getAttribute(sessionId.toString());
            if(session != null){
                return  session;
            }
        }
        Session session = super.retrieveSession(sessionKey);
        if(request != null && sessionId != null){
            request.setAttribute(sessionId.toString(),session);
        }
        return session;
    }
}

缓存管理

使用缓存存储不用总是去数据库中查找数据,提高效率

实现cache接口重写缓存操作

package com.xc.cache;

import com.xc.util.JedisUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.stereotype.Component;
import org.springframework.util.SerializationUtils;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.Set;

@Component
public class RedisCache<K,V> implements Cache<K,V> {

    @Resource
    private JedisUtil jedisUtil;

    private final String CACHE_PREFIX = "xc_cache";

    private byte[] getKey(K k){
        if(k instanceof String){
            return (CACHE_PREFIX + k).getBytes();
        }
        return SerializationUtils.serialize(k);
    }

    @Override
    public V get(K k) throws CacheException {
        System.out.println("从redis中获取授权数据");
        byte[] value = jedisUtil.get(getKey(k));
        if(value != null){
            return (V) SerializationUtils.deserialize(value);
        }
        return null;
    }

    @Override
    public V put(K k, V v) throws CacheException {
        byte[] key = getKey(k);
        byte[] value = SerializationUtils.serialize(v);
        jedisUtil.set(key,value);
        jedisUtil.expire(key,600);
        return v;
    }

    @Override
    public V remove(K k) throws CacheException {
        byte[] key = getKey(k);
        byte[] value = jedisUtil.get(key);
        jedisUtil.del(key);
        if(value != null){
            return (V) SerializationUtils.deserialize(value);
        }
        return null;
    }

    @Override
    public void clear() throws CacheException {

    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Set<K> keys() {
        return null;
    }

    @Override
    public Collection<V> values() {
        return null;
    }
}

实现CacheManager接口,重写缓存管理

package com.xc.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;

public class RedisCacheManager implements CacheManager {

    @Autowired
    private RedisCache redisCache;

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return redisCache;
    }
}

shiro默认认证是关闭缓存的,授权是开启的。所以要想认证也加入缓存,得手动开启

在这里插入图片描述

    <bean id="realm" class="com.xc.shiro.CustomReaml">
        <property name="credentialsMatcher" ref="matcher"></property>
<!--        <property name="authenticationCachingEnabled" value="true"></property>-->
<!--        <property name="authorizationCachingEnabled" value="true"></property>-->
    </bean>

注意

经过尝试我发现开启认证缓存的话,会因为没法序列化而报错

java.lang.IllegalArgumentException: Failed to serialize object of type: class org.apache.shiro.authc.SimpleAuthenticationInfo

细究其原因是因为org.apache.shiro.util.SimpleByteSource无法序列化

Caused by: java.io.NotSerializableException: org.apache.shiro.util.SimpleByteSource

然后我们找了一下发现SimpleByteSource是用来存储加密的盐值的

在这里插入图片描述

查找了该接口发现真的没有实现序列化

在这里插入图片描述

在这里插入图片描述

所以解决方案是重写一个类实现ByteSource接口来实现序列化,当然有人会问为什么不直接继承SimpleByteSource实现序列化就好

当然你也可以尝试一下,试了之后你会发现序列化是好了,但是反序列化会出错,会报无有效构造器的错误

java.io.InvalidClassException: com.springboot.test.shiro.config.shiro.MySimpleByteSource; no valid constructor

所以正确做法是重写一个类来实现ByteSource接口并实现序列化,可以直接复制SimpleByteSource的方法实现并写一个空参构造器

package com.xc.shiro;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;

import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;

public class MyByteSource implements ByteSource, Serializable {
    private final byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public MyByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public MyByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public MyByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public MyByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public MyByteSource(File file) {
        this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
    }

    public MyByteSource(InputStream stream) {
        this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public byte[] getBytes() {
        return this.bytes;
    }

    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }

        return this.cachedHex;
    }

    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    public String toString() {
        return this.toBase64();
    }

    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource)o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }
}

在自定义realm的认证中,修改使用到ByteSource的地方,即修改盐值

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //从主体传过来的认证信息中,获取用户名
        String userName = (String) authenticationToken.getPrincipal();

        //2.通过用户名到数据库中获取凭证
        String password = getPasswordByUserName(userName);

        if(password != null){
            SimpleAuthenticationInfo authenticationInfo =
                    new SimpleAuthenticationInfo(userName,password,"customRealm");

            authenticationInfo.setCredentialsSalt(new MyByteSource(userName)); //修改ByteSource
            return authenticationInfo;
        }
        return null;
    }

自动登录

/** = user表示访问该地址的用户是身份验证通过或RememberMe登录的都可以。

当过滤器为user时,只要有rememberme,就可以直接访问不用登陆

设置CookieRememberMeManager记住我管理,把属性注入进securitymanager

    <bean class="org.apache.shiro.web.mgt.CookieRememberMeManager" id="rememberMeManager">
        <property name="cookie" ref="cookie"></property>
    </bean>

    <bean class="org.apache.shiro.web.servlet.SimpleCookie" id="cookie">
        <constructor-arg value="rememberMe"/>
        <property name="maxAge" value="2000000"/> <!--单位秒-->
    </bean>
    <bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
        <property name="realm" ref="realm"></property>
        <property name="sessionManager" ref="sessionManager"></property>
        <property name="cacheManager" ref="cacheManager"></property>
        <property name="rememberMeManager" ref="rememberMeManager"></property>
    </bean>

在登陆的时候顺便提交是否记住我,在登录前都提交的数据设置记住我的属性

token.setRememberMe(user.isRememberMe());
subject.login(token);

整合spring类似,直接看源码,源码地址已分享

参考视频

源代码(练习和整合spring):https://gitee.com/xcsjavastudy/shiro-ssm

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值