本文将利用springboot集成shiro进行用户的登陆验证功能的开发实现。

springboot集成shiro需要引入以下依赖

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

实现登陆验证功能之前,我们需要先准备一些数据。

Shiro – 登陆验证-omgleoo

@Getter
@Setter
@ToString
public class User implements Serializable {
    private static final long serialVersionUID = -444037253400871944L;
    /** 用户编码 */
    private Integer id;
    /** 用户名 */
    private String userName;
    /** 账户密码 */
    private String password;
    /** 创建日期 */
    private Date createTime;
    /** 账户状态 1正常 0锁定 */
    private String status;
}

数据准备好之后,就可以进行登陆验证功能的开发了。(数据操作层省略,具体可以下载代码进行查阅)

shiro进行登陆验证的过程大致可以归纳为以下几点

  1. ShiroConfig中配置SecurityManagerBean,SecurityManager为shiro的安全管理器,subject由他统一管理。具体概念可以参考https://www.omgleoo.top/%E4%BA%86%E8%A7%A3shiro/
  2. 在ShiroConfig中配置ShiroFilterFactoryBean,他是Shiro过滤器工厂类,依赖SecurityManager。
  3. 根据自己的需求,自定义Realm。
    Realm包含doGetAuthorizationInfo()doGetAuthenticationInfo()方法 。分别为登陆认证和权限认证。

ShiroConfig

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSuccessUrl("index");
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 静态文件,设置不拦截
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/images/**", "anon");
        filterChainDefinitionMap.put("/sass/**", "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(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        // shiro 生命周期处理器
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 该Realm需要用户自定义
     * @return
     */
    @Bean
    public ShiroRealm shiroRealm(){
        // 配置Realm
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }
}

过滤器filterChain是基于短路机制实现的(最先匹配原则)。

anon,authc,logout等。是shiro为我们实现的过滤器。

anon

org.apache.shiro.web.filter.authc.AnonymousFilter
匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例/static/**=anon

authc

org.apache.shiro.web.filter.authc.FormAuthenticationFilter
基于表单的拦截器;如/**=authc,如果没有登录会跳到相应的登录页面登录

logout

org.apache.shiro.web.filter.authc.LogoutFilter
退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/),示例/logout=logout

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端口,其他路径/参数等都一样

roles

org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
角色授权拦截器,验证用户是否拥有所有角色;示例/admin/**=roles[admin]

user

org.apache.shiro.web.filter.authc.UserFilter
用户拦截器,用户已经身份验证/记住我登录的都可;示例/**=user

ShiroConfig配置完成之后,我们根据需求实现Realm,然后将其注入到SecurityManager中。

Realm

自定义Realm需要集成AuthorizingRealm类,然后重写 doGetAuthorizationInfo()和doGetAuthenticationInfo()方法即可。 这一节我们只实现登陆验证功能,所以之重写
doGetAuthorizationInfo ()即可。

@Slf4j
public class ShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserMapper userDao;

    /**
     * 获取用户角色和权限(暂不实现)
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 登陆认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取界面中输入的用户名和密码
        String username = (String) authenticationToken.getPrincipal();
        String password = new String((char[]) authenticationToken.getCredentials());
        log.info("认证登录:username is {}", username);

        // 通过用户名获取用户信息
        User user = userDao.getByName(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;
    }
}

页面准备:

编写测试页面login.html,index.html

编写LoginController

@Controller
public class LoginController {
    /**
     * 登陆
     * @return
     */
    @GetMapping("/login")
    public String login(){
        return "login";
    }

    @PostMapping("/login")
    @ResponseBody
    public Result login(String username, String password, Boolean rememberMe) {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            return Result.ok();
        } catch (UnknownAccountException e) {
            return Result.error(e.getMessage());
        } catch (IncorrectCredentialsException e) {
            return Result.error(e.getMessage());
        } catch (LockedAccountException e) {
            return Result.error(e.getMessage());
        } catch (AuthenticationException e) {
            return Result.error("认证失败!");
        }
    }

    @RequestMapping("/")
    public String redirectIndex() {
        return "redirect:/index";
    }

    @RequestMapping("/index")
    public String index(Model model) {
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        model.addAttribute("user", user);
        return "index";
    }
}

登录成功后,根据之前在ShiroConfig中的配置shiroFilterFactoryBean.setSuccessUrl("/index"),页面会自动访问/index路径。

接下来,就可以欣赏自己的成果了。http://localhost:8080

Shiro – 登陆验证-omgleoo

测试用户:

正常用户 admin/admin 锁定用户test/test

RememberMe功能

当用户成功登录之后,关闭浏览器然后再次访问该网址时,页面会再次跳转到登陆页面。之前的登陆已经失效。

下面我们开发一个Remember Me功能(Shiro为我们提供了Remember功能)。使用户的登陆状态不会因为浏览器的关闭而失效。

首先在ShiroConfig中添加cookie对象

/**
 * cookie对象
 * @return
 */
public SimpleCookie rememberMeCookie() {
    // 设置cookie名称,对应login.html页面的<input type="checkbox" name="rememberMe"/>
    SimpleCookie cookie = new SimpleCookie("rememberMe");
    // 设置cookie的过期时间,单位为秒,这里为一小时
    cookie.setMaxAge(3600);
    return cookie;
}

/**
 * cookie管理对象
 * @return
 */
public CookieRememberMeManager rememberMeManager() {
    CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
    cookieRememberMeManager.setCookie(rememberMeCookie());
    // rememberMe cookie加密的密钥 
    cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
    return cookieRememberMeManager;
}

接下来将cookie对象设置到SecurityManager中

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

将ShiroFilterFactoryBean的filterChainDefinitionMap.put("/**", "authc");更改为filterChainDefinitionMap.put("/**", "user"); user的功能上面有详细介绍。

功能开发完成之后,我们分别在html页面和LoginController中加入RememberMe的相关代码即可。

@PostMapping("/login")
@ResponseBody
public Result login(String username, String password, Boolean rememberMe) {
	UsernamePasswordToken token = new UsernamePasswordToken(username, password);
	Subject subject = SecurityUtils.getSubject();
	try {
		subject.login(token);
		return Result.ok();
	} catch (UnknownAccountException e) {
		return Result.error(e.getMessage());
	} catch (IncorrectCredentialsException e) {
		return Result.error(e.getMessage());
	} catch (LockedAccountException e) {
		return Result.error(e.getMessage());
	} catch (AuthenticationException e) {
		return Result.error("认证失败!");
	}
}
<p><input type="checkbox" name="rememberMe" />记住我</p>

<script th:inline="javascript"> 
    var ctx = [[@{/}]];
    function login() {
        var username = $("input[name='username']").val();
        var password = $("input[name='password']").val();
        var rememberMe = $("input[name='rememberMe']").is(':checked');
        $.ajax({
            type: "post",
            url: ctx + "login",
            data: {"username": username,"password": password,"rememberMe": rememberMe},
            dataType: "json",
            success: function (r) {
                if (r.code == 0) {
                    location.href = ctx + 'index';
                } else {
                    alert(r.msg);
                }
            }
        });
    }
</script>

当rememberMe参数为true的时候,Shiro就会帮我们记住用户的登录状态。启动项目即可看到效果。