SpringBoot/SpringMVC整合Shiro:实现登录与注册(MD5加盐加密)

本文转载于:https://blog.csdn.net/Colton_Null/article/details/78992836

----------------------------------超级嚣张的分割线---------------------------------------

终于终于,用正确姿势搭建了一个Spring框架下整合Shiro的登录注册的DEMO。

因为还有其他事情要忙,所以有关这个shiro的demo前前后后鼓捣了一周多。为啥多花这么多时间?

按照平常,我在研究新框架的时候,都是看看框架介绍,找找样例几天就能搞出来了。但这次最气的是,目前能够讲清楚Spring + Shiro整合方法认真写的博客几乎没有。照着几个博客跟着配置,越配越生气的。给的配置文件不全;夹杂着好多没用的配置文件;没有注释;代码不全;还有的用法直接就是错的。耽误了太多的时间。

所以,就自己整理下正确实现Spirng + Shiro整合的姿势。分享给大家,希望别再走弯路。文末有demo源码,大家可以自行下载。

本文主要基于SpringBoot + Maven + Mybatis实现对Shiro的整合。对于Mabatis的配置在这里就不多做介绍了。而对于SpringMVC,也是SSM框架,只不过在对Shiro的配置方式上有所不同,原理都是一样的。我也会把SpringMVC下Shiro的配置方法贴出来。

在这里推荐两个个人觉得受益颇多的教程链接:
《跟我学shiro》
腾讯课程视频《Shiro安全框架》

一、准备好Maven + SpringBoot + MyBatis开发环境
1.准备一个user_t表,用户存放账户信息

CREATE TABLE `user_t` (
  `id` varchar(32) NOT NULL,
  `username` varchar(64) NOT NULL,
  `password` varchar(64) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS=1;


2.编写两个DAO接口
分别用于根据username查询信息以及插入一条数据。
UserMapper.java

@Repository
public interface UserMapper {

    /**
     * 根据用户名查询用户信息
     * @param username 用户名
     * @return 将数据封装到Map类型中
     */
    public Map<String, Object> queryInfoByUsername(String username);

    /**
     * 插入一条数据
     * @param data Map中包含id,username,password
     */
    public void insertData(Map<String, String> data);
}


userMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myz.shirodemo.dao.UserMapper">

    <select id="queryInfoByUsername" parameterType="java.lang.String" resultType="java.util.Map">
        SELECT id, username, password FROM user_t WHERE username = #{username,jdbcType=VARCHAR}
    </select>

    <insert id="insertData"  parameterType="java.util.Map">
        INSERT INTO user_t ( id, username,password )
        VALUES ( #{id, jdbcType=VARCHAR}, #{username, jdbcType=VARCHAR},#{password, jdbcType=VARCHAR});
    </insert>
</mapper>


二、引入Shiro依赖
pom中有关shiro的依赖如下

<!-- shiro -->
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-all</artifactId>
   <version>1.2.2</version>
</dependency>
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-core</artifactId>
   <version>1.2.2</version>
</dependency>

<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-web</artifactId>
   <version>1.2.2</version>
</dependency>

<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-ehcache</artifactId>
   <version>1.2.2</version>
</dependency>

<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.2.2</version>
</dependency>
<!-- shiro END-->


三、配置ShiroConfig类
ShiroConfig.java

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 拦截器。匹配原则是最上面的最优先匹配
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
        // 配置不会被拦截的链接
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/doLogin", "anon");
        filterChainDefinitionMap.put("/doRegister", "anon");
        filterChainDefinitionMap.put("/register", "anon");

        // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/doLogout", "logout");
        
        // 剩余请求需要身份认证
        filterChainDefinitionMap.put("/**", "authc");
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");

        // 未授权界面;
//        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean(name = "myShiroRealm")
    public ShiroRealm myShiroRealm(HashedCredentialsMatcher matcher){
        ShiroRealm myShiroRealm = new ShiroRealm();
        myShiroRealm.setCredentialsMatcher(matcher);
        return myShiroRealm;
    }


    @Bean
    public SecurityManager securityManager(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher){
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm(matcher));
        return securityManager;
    }

    /**
     * 密码匹配凭证管理器
     *
     * @return
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 采用MD5方式加密
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        // 设置加密次数
        hashedCredentialsMatcher.setHashIterations(1024);
        return hashedCredentialsMatcher;
    }
}


1.shirFilter(SecurityManager securityManager)方法,是设置shiro的过滤规则。用于控制哪些请求需要身份认证后才能继续执行,哪些不需要认证等。
从http://blog.csdn.net/u010092167/article/details/52372811这个博主的博文中找到拦截器汇总表。

身份验证相关:

  • authc:基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址;failureKeyAttribute:登录失败后错误信息存储key(shiroLoginFailure);
  • authcBasic:Basic HTTP身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application);
  • logout:退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/);示例“/logout=logout”
  • user:用户拦截器,用户已经身份验证/记住我登录的都可;示例“/**=user”
  • anon:匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon”

授权相关的:

  • roles:角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]”
  • perms:权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms[“user:create”]”
  • port:端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
  • rest:rest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll);
  • ssl:SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样;

其他:

  • noSessionCreation:不创建会话拦截器,调用 subject.getSession(false)不会有什么问题,但是如果subject.getSession(true)将抛出 DisabledSessionException异常;
  • myShiroRealm(HashedCredentialsMatcher matcher)用于配置自定义的Realm。在Shiro中,所有有关身份认证及授权管理数据源的获取与管理,都在Realm中进行。
  • hashedCredentialsMatcher()用于生成加密规则。这里采用MD5加密1024次的方式对密码进行加密处理。
  • securityManager(HashedCredentialsMatcher matcher)将加密规则属性设置到自定义的ShiroRealm中,并将这个Realm加载到SecurityManager中。

四、配置自定义Realm
ShiroRealm.java

public class ShiroRealm extends AuthenticatingRealm {
    @Autowired
    private BaseService baseService;

    private SimpleAuthenticationInfo info = null;

    /**
     * 1.doGetAuthenticationInfo,获取认证消息,如果数据库中没有数,返回null,如果得到了正确的用户名和密码,
     * 返回指定类型的对象
     *
     * 2.AuthenticationInfo 可以使用SimpleAuthenticationInfo实现类,封装正确的用户名和密码。
     *
     * 3.token参数 就是我们需要认证的token
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 将token装换成UsernamePasswordToken
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        // 获取用户名即可
        String username = upToken.getUsername();
        // 查询数据库,是否查询到用户名和密码的用户
        Map<String, Object> userInfo = baseService.queryInfoByUsername(username);

        if(userInfo != null) {
            // 如果查询到了,封装查询结果,返回给我们的调用
            Object principal =  userInfo.get("username");
            Object credentials = userInfo.get("password");
            
            // 获取盐值,即用户名
            ByteSource salt = ByteSource.Util.bytes(username);
            String realmName = this.getName();
            // 将账户名,密码,盐值,realmName实例化到SimpleAuthenticationInfo中交给Shiro来管理
            info = new SimpleAuthenticationInfo(principal, credentials, salt,realmName);
        }else {
            // 如果没有查询到,抛出一个异常
            throw new AuthenticationException();
        }
        return info;
    }
}


1.这里我只做了身份认证。新建一个ShiroRealm类继承AuthenticatingRealm类,实现doGetAuthenticationInfo(AuthenticationToken authenticationToken)方法。

2.这个方法主要就是用于获取数据库中的账户信息,以便用于和用户登录时从前台传过来的账户密码进行对比。

3.根据用户名到用户表中查询账户名密码,并设置好盐值。这里的盐值要和ShiroConfig中的盐值规则一样。将账户名,密码,盐值,realmName实例化到SimpleAuthenticationInfo中交给Shiro来管理。

4.如果账户不存在,则抛出AuthenticationException异常。

5.这样,每次用户进行login操作时,就会调用doGetAuthenticationInfo方法。Shiro就自动帮我们校验了账户密码是否匹配。

五、实现登录
这里先贴上Controller的全部代码
MyController.java

@Controller
public class MyController {
    @Autowired
    private BaseService baseService;

    private final Logger logger = LoggerFactory.getLogger(MyController.class);

    @RequestMapping("/doLogin")
    public String doLogin(@RequestParam("username") String username,
                          @RequestParam("password") String password) {
        // 创建Subject实例
        Subject currentUser = SecurityUtils.getSubject();

        // 将用户名及密码封装到UsernamePasswordToken
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try {
            currentUser.login(token);
            // 判断当前用户是否登录
            if (currentUser.isAuthenticated() == true) {
                return "/index.html";
            }
        } catch (AuthenticationException e) {
            e.printStackTrace();
            System.out.println("登录失败");
        }
        return "/loginPage.html";
    }

    @RequestMapping("/doRegister")
    public String doRegister(@RequestParam("username") String username,
                             @RequestParam("password") String password) {
        boolean result = baseService.registerData(username,password);
        if(result){
            return "/login";
        }
        return "/register";
    }

    @RequestMapping(value = "/login")
    public String login() {
        logger.info("login() 方法被调用");
        return "loginPage.html";
    }

    @RequestMapping(value = "/register")
    public String register() {
        logger.info("register() 方法被调用");
        return "registerPage.html";
    }

    @RequestMapping(value = "/hello")
    public String hello() {
        logger.info("hello() 方法被调用");
        return "helloPage.html";
    }
}


1.在doLogin方法中,实现登录认证过程。

2.首先获取当前Subject实例

3.将用户名和密码封装到UsernamePasswordToken中

4.用当前Subject实例执行login方法,传入参数为刚刚封装的token。执行login方法后,shiro框架最终就会调用刚刚自定义ShiroRealm中的doGetAuthenticationInfo方法。

5.用isAuthenticated()方法判断用户是否已经登录,如果是则跳转到登录后的页面(这里我跳转到的是index.html)。如果登录失败,则走报异常,最后还是跳转到登录界面。

6.这里我只catch了AuthenticationException异常。然而在AuthenticationException下有多个子异常,用于各种登录失败的场景,比如账户名不存在,密码不对,登录次数过多等等。大家针对不同的情况做不同的处理。但有一点建议,就是对于前台用户来说,不要暴露过多的错误信息,只是报一个登录失败即可,提高安全性。

六、实现注册
在Controller的doRegister方法中,调用service层的registerData方法,完成注册功能。

在service中对DAO进行封装,实现信息查询以及信息注册。
接口BaseService.java

public interface BaseService {

    /**
     * 根据用户名查询用户信息
     * @param username 用户名
     * @return 将数据封装到Map类型中
     */
    public Map<String, Object> queryInfoByUsername(String username);

    /**
     * 注册功能
     * @param username 用户名
     * @param password 密码
     * @return
     */
    public boolean registerData(String username, String password);
}


BaseService接口的实现类
BaseServiceImpl.java

@Service
public class BaseServiceImpl implements BaseService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public Map<String, Object> queryInfoByUsername(String username) {
        return userMapper.queryInfoByUsername(username);
    }

    @Override
    public boolean registerData(String username, String password) {
        // 生成uuid
        String id = UUIDUtil.getOneUUID();

        // 将用户名作为盐值
        ByteSource salt = ByteSource.Util.bytes(username);
        /*
        * MD5加密:
        * 使用SimpleHash类对原始密码进行加密。
        * 第一个参数代表使用MD5方式加密
        * 第二个参数为原始密码
        * 第三个参数为盐值,即用户名
        * 第四个参数为加密次数
        * 最后用toHex()方法将加密后的密码转成String
        * */
        String newPs = new SimpleHash("MD5", password, salt, 1024).toHex();

        Map<String, String> dataMap = new HashMap<>();
        dataMap.put("id", id);
        dataMap.put("username", username);
        dataMap.put("password", newPs);

        // 看数据库中是否存在该账户
        Map<String, Object> userInfo = queryInfoByUsername(username);
        if(userInfo == null) {
            userMapper.insertData(dataMap);
            return true;
        }
        return false;
    }
}


1.注册时注意,由于之前配置了盐值规则及加密规则,所以这里要对用户输入的密码也做相同的处理之后再存入数据库中。

2.使用SimpleHash类完成密码的加密。最后用toHex()将加密后的密码转成String。

七、大功告成
到此,有关SpringBoot与Shiro的整合就基本完成了。有关静态页的代码这里就不贴了,在下面的github源码中有,大家可以自行下载。

最终效果就是,在http://localhost/register界面中,完成账户的注册。在http://localhost/login界面中进行登录操作。如果登录成功,则跳转到index页。如果登录失败或者没有登录,则访问index时会自动跳转到login界面。

八、有关SpringMVC环境下的配置
在SpringMVC下,有关Shiro的配置就要在Spring配置文件中来完成。
配置原理和SpringBoot中的一样。
spring-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login"/>
        <property name="successUrl" value="/views/success.jsp"/>
        <property name="unauthorizedUrl" value="/views/test.jsp"/>
        <!-- 配置拦截策略 -->
        <property name="filterChainDefinitions">
            <value>
                /login = anon
                /doLogin = anon
                /doLogout = logout
                /** = authc
            </value>
        </property>
    </bean>

    <!-- 自定义Realm -->
    <bean id="myRealm" class="com.myz.bean.ShiroRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="MD5"/>
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
    </bean>

    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 缓存管理器 -->
        <property name="cacheManager" ref="cacheManager" />
        <property name="realm" ref="myRealm" />
    </bean>

    <!-- 配置ehcache -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
    </bean>

    <!-- 用来管理Spring容器中的Shiro常见的对象 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

    <!-- 网络方面 -->
    <bean id="secureRemoteInvocationExecutor" class="org.apache.shiro.spring.remoting.SecureRemoteInvocationExecutor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

</beans>


shiro缓存配置
ehcache-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false"  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>
    <cache name="shiro_cache"
           maxElementsInMemory="2000"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="0"
           maxElementsOnDisk="0"
           overflowToDisk="true"
           memoryStoreEvictionPolicy="FIFO"
           statistics="true">
    </cache>
</ehcache>


九、源码
有关SpringBoot + Maven + Mybatis + Shiro的登录注册demo源码:大佬Github的链接!

感谢大佬的文章讲解和源码提供!!!
 

猜你喜欢

转载自blog.csdn.net/Janson_Lin/article/details/85336731