shiro用户认证

springboot集成shiro的用户认证过程如下:

  1. 定义一个ShiroConfig类,实现Shiro过滤器工厂类ShiroFilterFactoryBean方法,该方法主要用于处理拦截资源文件,依赖SecurityManager。
  2. SecurityManager为Shiro的安全管理器,管理着所有Subject,主要有身份认证管理,缓存管理,cookie管理等。
  3. realm,用于身份认证和权限验证,需要实现doGetAuthorizationInfo() 和 doGetAuthenticationInfo() 方法。

引入相关依赖

引入shiro、数据源,mybatis依赖,

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!-- shiro-spring -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.1</version>
    </dependency>

    <!-- druid数据源驱动 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

定义shiroconfig类

@Configuration
public class ShiroConfig {
    /**
     * springboot之shiro用户认证 处理拦截资源文件问题。
     * 注意:单独一个ShiroFilterFactoryBean配置是报错的,因为在 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
     *
     Filter Chain定义说明
     1、一个URL可以配置多个Filter,使用逗号分隔
     2、当设置多个过滤器时,全部验证通过,才视为通过
     3、部分过滤器可指定参数,如perms,roles
     *
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSuccessUrl("/index");
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

    LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
  //  anon:所有url都都可以匿名访问;  authc: 需要认证才能进行访问; user:配置记住我或认证通过可以访问;
    filterChainDefinitionMap.put("/css/**", "anon");
    filterChainDefinitionMap.put("/js/**", "anon");
    filterChainDefinitionMap.put("/fonts/**", "anon");
    filterChainDefinitionMap.put("/img/**", "anon");
    filterChainDefinitionMap.put("/druid/**", "anon");
    filterChainDefinitionMap.put("/logout", "logout");
    filterChainDefinitionMap.put("/", "anon");
    filterChainDefinitionMap.put("/**", "authc");

    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

    return shiroFilterFactoryBean;
}

@Bean
public SecurityManager securityManager(){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(shiroRealm());
    return securityManager;
}

@Bean
public ShiroRealm shiroRealm(){
    ShiroRealm shiroRealm = new ShiroRealm();
    return shiroRealm;
}
}

过滤链filterchain基于短路机制,是最先匹配原则;
anon:所有url都可以匿名访问;
authc: 需要认证才能进行访问;
user:配置记住我或认证通过可以访问;
其中,shiro已实现anon,authc…

FilterNmae class description
anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例 /static/**=anon
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基于表单的拦截器;如 /**=authc ,如果没有登录会跳到相应的登录页面登录
user org.apache.shiro.web.filter.authc.UserFilter 用户拦截器,用户已经身份验证/记住我登录的都可;示例 /**=user
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP身份验证拦截器
logout org.apache.shiro.web.filter.authc.LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/),示例 /logout=logout
role org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;示例 /admin/**=roles[admin]
noSessionCreation org.apache.shiro.web..filter.session.NoSessionCreationFilter 不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例 /user/**=perms[“user:create”]
port org.apache.shiro.web.filter.authz.PortFilter 端口拦截器,主要属性 port(80) :可以通过的端口;示例 /test= port[80] ,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter rest风格拦截器,自动根据请求方法构建权限字符串;示例 /users=rest[user] ,会自动拼出user:read,user:create,user:update,user:delete权限字符串进行权限匹配(所有都得匹配,isPermittedAll)
ssl org.apache.shiro.web.filter.authz.SslFilter SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口443;其他和port拦截器一样;

Realm

自定义Realm实现只需继承AuthorizingRealm类,然后实现doGetAuthorizationInfo()和doGetAuthenticationInfo()方法即可。这两个方法名乍看有点像,doGetAuthorizationInfo()获取用户的角色和权限等信息,doGetAuthenticationInfo()进行身份验证,即登录时验证用户的合法性,比如验证用户名和密码。

Shiro的认证过程会交由Realm执行,会调用Realm的getAuthenticationInfo(token)方法。

该方法主要执行逻辑如下:

1、检查提交的进行认证的令牌信息

2、根据令牌信息从数据源(通常为数据库)中获取用户信息

3、对用户信息进行匹配验证。

4、验证通过将返回一个封装了用户信息的AuthenticationInfo实例。

5、验证失败则抛出AuthenticationException异常信息。

/**
 * 登录认证
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    String userName  = (String) token.getPrincipal();
    String password = new String((char[]) token.getCredentials()) ;
  //  password= MD5Utils.encrypt(userName,password);
    System.out.println("用户" + userName + "认证-----ShiroRealm.doGetAuthenticationInfo" +"password: "+password);
    User user = userMapper.findByUserName(userName);


    if(user == null){
        throw new UnknownAccountException("用户名或密码错误!");
    }
    if (!password.equals(user.getPassword())) {
        throw new IncorrectCredentialsException("用户名或密码错误!");
    }
    if (user.getStatus().equals("0")) {
        throw new LockedAccountException("账号已被锁定,请联系管理员!");
    }
    SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
    return info;

}

配置文件application.yml

spring:
  datasource:
    # 数据库访问配置, 使用druid数据源
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro
    username: root
    password: root
    # 连接池配置
    initial-size: 5
    min-idle: 5
    max-active: 20
    # 连接等待超时时间
    max-wait: 30000
    # 配置检测可以关闭的空闲连接间隔时间
    time-between-eviction-runs-millis: 60000
    # 配置连接在池中的最小生存时间
    min-evictable-idle-time-millis: 300000
    validation-query: select '1' from dual
    test-while-idle: true
    test-on-borrow: false
    test-on-return: false

 thymeleaf:
  cache: false
mybatis:
  # type-aliases扫描路径
  type-aliases-package: com.springboot.shiro.entity
  # mapper xml实现扫描路径
  mapper-locations: classpath:mapper/*.xml
  property:
   order: BEFORE

定义数据库

创建t_user表
USE shiro;

/*Table structure for table `t_user` */

DROP TABLE IF EXISTS `t_user`;

CREATE TABLE `t_user` (
  `id` int(11) NOT NULL,
  `username` varchar(25) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `create_time` date DEFAULT NULL,
  `status` varchar(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `t_user` */

insert  into `t_user`(`id`,`username`,`password`,`create_time`,`status`) values (1,'admin','9f953fb5518da53fe1dc83f47861cb6d','2019-04-11','1'),(2,'test','d655eb5f87c32223e3d037b760a0341d','2019-04-11','0');

定义实体类

定义实体user类

public class User {
    private Integer id;
    private String userName;
    private String password;
    private Date createTime;
    private String status;

    //get/set方法
}

定义logincontroller类

@PostMapping("/login")
@ResponseBody
public ActionResult login(String username,String password){
    password = MD5Utils.encrypt(username,password);
    UsernamePasswordToken token = new UsernamePasswordToken(username,password);

    Subject subject = SecurityUtils.getSubject();

    try {
        subject.login(token);
        return ActionResult.ok();
    } catch (UnknownAccountException e) {
        return ActionResult.error(e.getMessage());
    } catch (IncorrectCredentialsException e) {
        return ActionResult.error(e.getMessage());
    } catch (LockedAccountException e) {
        return ActionResult.error(e.getMessage());
    }catch (AuthenticationException e) {
        return ActionResult.error("认证失败");

    }

}

编写页面login.html、index.html页面等。

启动项目访问:

地址:http://localhost:8080/login

账号或密码错误提示:

账号锁定提示:

登录成功后直接跳转到index界面。

rememberMe功能的实现

shiro提供了rememberMe功能,就是在登录页面加上记住检验框,用户勾选记住登录后,实质上是添加了一个本地的cookie,关闭浏览器后再访问就不用进行重新登录:

<p><input type="checkbox" name="rememberMe"/>记住我</p>

修改shiroconfig

在shiroconfig类中添加rememberMeManager对象,再讲cookie管理对象注入到securityManager中,

public SimpleCookie rememberMeCookie(){
    System.out.println("ShiroConfig +rememberMeCookie()" );
    // 设置cookie名称,对应login.html页面的<input type="checkbox" name="rememberMe"/>
    SimpleCookie cookie = new SimpleCookie("rememberMe");
 //   cookie.setName("shiroCookie");
    cookie.setMaxAge(86400);
    return cookie;
}

public CookieRememberMeManager rememberMeManager(){
    System.out.println("ShiroConfig +rememberMeManager()" );
    CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
    cookieRememberMeManager.setCookie(rememberMeCookie());
    return cookieRememberMeManager;
}

@Bean
public SecurityManager securityManager(){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(shiroRealm());
    securityManager.setRememberMeManager(rememberMeManager());
    return securityManager;
}

修改权限配置

修改shiroFilterFactoryBean中的权限过滤,修改authc为user,即用户通过认证或勾选了rememberMe之后记住用户的信息,直接可以访问,主要修改以下两项,用户可以直接index页面

//配置记住我事调用 user
  filterChainDefinitionMap.put("/index","user");
  filterChainDefinitionMap.put("/","user");

最后在登录页面添加

<input type="checkbox" name="rememberMe"/>记住我

和修改对应的logincontroller即可实现。

参考:

springbootshiro权限管理

SpringBoot Shiro用户认证