Spring Boot 整合SSM+Thymeleaf+Shiro+EhCache实现登录认证和授权功能

文 | 平哥 日期 | 20200927

用于记录自己Spring Boot整合Shiro过程。
官方有篇教程可以参考:Integrating Apache Shiro into Spring-Boot Applications

基本环境和工具
IDE: IntelliJ IDEA
Maven: 3.6.0
JDK: 1.8

Step 1 搭建基础SSM环境

1.1 创建Maven工程,添加SSM+Thymeleaf+Shiro依赖

Step1 创建Maven工程:
省略用IDEA添加Maven工厂项目步骤,这个默认大家都懂……
提示:新建项目后记得配置IDEA的Maven参数,改为本地自己安装的Maven

Step2 添加Spring Boot父依赖、shiro依赖:



    org.springframework.boot
    spring-boot-starter-parent
    2.3.4.RELEASE


    
    
        org.apache.shiro
        shiro-spring-boot-web-starter
        1.6.0
    
    
    
        org.springframework.boot
        spring-boot-starter-thymeleaf
    
    
    
        org.mybatis.spring.boot
        mybatis-spring-boot-starter
        2.1.3
    
    
    
        mysql
        mysql-connector-java
        8.0.11
    

1.2 配置application.yml中数据库信息

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root

1.3 编写MVC各层代码

编写基本MVC各层包和目录以及相关代码:


MVC目录结构

1.4 编写Thymeleaf页面

Spring Boot 整合Thymeleaf,无需进行任何配置,只需在resources文件夹下创建templates文件夹,在其中创建html文件,SpringMVC即可自动进行跳转:

页面和静态资源文件结构

JS、css、图片等静态资源须放在static文件夹

对于公共页面的跳转可以利用路径变量创建一个公用的单元方法:

// 公共页面跳转共用单元方法
@RequestMapping("/{path}")
public String getPage(@PathVariable String path){
    return path;
}

Step 2 整合Shiro,实现登录认证

注意:相关依赖已在第一步导入,不在赘述。

2.1 配置application.yml

在Spring Boot配置文件application.yml中配置Shiro的默认登录链接:

shiro:
  loginUrl: /login

2.2 编写自定义Realm类和Shiro配置类

Step1 创建com.gcp.shiro包
Step2 在其中创建MyRealm类,继承AuthorizingRealm类,重写认证方法:

/*重写认证方法*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("认证用户中……");
    // 从token获取用户登录输入的用户名
    String unameFromWeb = token.getPrincipal().toString();
    // 利用用户名去数据库查询是否有数据
    User user = userService.selectUserByUname(unameFromWeb);
    if (user!=null) {
        AuthenticationInfo info = new SimpleAuthenticationInfo(token.getPrincipal(), user.getPwd(),
                ByteSource.Util.bytes("gcp"), token.getPrincipal().toString());
        return info;
    }
    return null;
}

Step3 创建ShiroConfig类,配置SecurityManager bean和Shiro内置过滤器bean:

@Configuration
public class ShiroConfig {
    @Autowired
    private MyRealm myRealm;
    // 配置Security Manager
    @Bean
    public DefaultWebSecurityManager getSecurityManager(){
        // 实例化SecurityManager
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 实例化Shiro默认的密码匹配器
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        // 设置密码加密算法为md5
        matcher.setHashAlgorithmName("md5");
        // 设置迭代次数
        matcher.setHashIterations(2);
        // 将密码匹配器加入自定义realm中:
        myRealm.setCredentialsMatcher(matcher);
        // 将自定义的realm集成到DefaultWebSecurityManager对象中
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    // 配置Shiro默认过滤器
    @Bean
    public ShiroFilterChainDefinition getFilter(){
        DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();
        // 放行公共的页面和静态资源的访问
        filterChainDefinition.addPathDefinition("/login","anon");
        filterChainDefinition.addPathDefinition("/css/**","anon");
        filterChainDefinition.addPathDefinition("/js/**","anon");
        filterChainDefinition.addPathDefinition("/images/**","anon");
        filterChainDefinition.addPathDefinition("/themes/**","anon");
        filterChainDefinition.addPathDefinition("/userLogin","anon");
        // 其余必须登录才能访问
        filterChainDefinition.addPathDefinition("/**","user");
        return filterChainDefinition;
    }
}

2.3 编写用户登录验证单元方法

在登录的PulicController类中:

/**
 * 用户登录方法
 * @param uname
 * @param pwd
 * @return
 */
@RequestMapping("userLogin")
@ResponseBody
public Result userLogin(String uname, String pwd){
    // 利用用户名密码实例化Shiro token
    UsernamePasswordToken tonken = new UsernamePasswordToken(uname, pwd);
    try {
        // 获取Subject并进行登录
        SecurityUtils.getSubject().login(tonken);
        return new Result();
    }catch (AuthenticationException e) {
        e.printStackTrace();
        return new Result("用户名或密码不匹配");
    }
}

此处login方法会调用MyRealm的认证方法进行匹配。此时项目即可实现用户登录功能了。

2.4 用Shiro实现remember me

Shiro实现记住我功能十分简单:在Shiro的配置文件进行如下修改:
Step1 在设置SecurityManager方法中修改:(添加设置Shiro的remember me 功能)

// 配置Security Manager
@Bean
public DefaultWebSecurityManager getSecurityManager(){
    // 实例化SecurityManager
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 实例化Shiro默认的密码匹配器
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    // 设置密码加密算法为md5
    matcher.setHashAlgorithmName("md5");
    // 设置迭代次数
    matcher.setHashIterations(2);
    // 将密码匹配器加入自定义realm中:
    myRealm.setCredentialsMatcher(matcher);
    // 将自定义的realm集成到DefaultWebSecurityManager对象中
    securityManager.setRealm(myRealm);
    //设置Shiro的remember me功能
    securityManager.setRememberMeManager(rememberMeManager());
    return securityManager;
}

rememberMeManager()方法代码:

// 设置shiro的remembermeManager
private RememberMeManager rememberMeManager() {
    // 实例化shiro的remembermeManager
    CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
    // 设置cookie的有效期
    cookieRememberMeManager.setCookie(rememberMeCookie());
    // 添加Cookie加密设置
    cookieRememberMeManager.setCipherKey("123123123".getBytes());
    return cookieRememberMeManager;
}

rememberMeCookie()方法代码:

// 设置Cookie参数
private SimpleCookie rememberMeCookie() {
    SimpleCookie cookie = new SimpleCookie();
    cookie.setPath("/");
    cookie.setHttpOnly(true);
    // 单位是秒
    cookie.setMaxAge(3*24*60*60);
    return cookie;
}

Step2 设置好后,再次修改用户登录的单元方法:添加一个boolean类型的rememberme参数,并且设置默认值为false:

/**
 * 用户登录方法
 */
@RequestMapping("userLogin")
@ResponseBody
public Result userLogin(String uname, String pwd,@RequestParam(defaultValue = "false") Boolean rememberme){
    // 利用用户名密码实例化Shiro token
    UsernamePasswordToken tonken = new UsernamePasswordToken(uname, pwd,rememberme);
   // 省略余下代码,余下代码没变化,相见上部
}

Step 3 Shiro实现后台功能及页面显示授权

鉴权就是判断用户是否有权限执行相应方法或看到页面具体内容
授权就是授予认证用户指定的角色或指定的权限。

3.1 后台功能方法鉴权

Shiro在后台可以用在控制器方法,也可以用在业务方法。通常都在控制器方法上添加注解进行鉴权。
本项目中具体用户的增删改查四个方法,利用注解@RequirePermissions("要求的权限")进行分别鉴权,具体代码如下:
新建UserController类:

@Controller
public class UserController {
    //声明单元方法:用户新增
    @RequiresPermissions("user:add")
    @RequestMapping("userAdd")
    @ResponseBody
    public String userAdd(){
        System.out.println("新增用户单元方法执行。");
        return "恭喜,新增用户成功!";
    }
    //声明单元方法:用户删除
    @RequiresPermissions("user:del")
    @RequestMapping("userDel")
    @ResponseBody
    public String userDel(){
        System.out.println("用户信息删除单元方法执行");
        return "恭喜,用户删除成功!";
    }
    //声明单元方法:用户修改
    @RequiresPermissions("user:edit")
    @RequestMapping("userEdit")
    @ResponseBody
    public String userEdit(){
        System.out.println("用户信息修改单元方法执行");
        return "恭喜,用户修改成功!";
    }
}

3.2 Thymeleaf页面中鉴权

在需要鉴权的页面中,在标签中添加属性:
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"

在具体的需要进行鉴权的页面元素中使用shiro标识,例如:


还需要再添加Thymeleaf整合Shiro的依赖:



    com.github.theborakompanioni
    thymeleaf-extras-shiro
    2.0.0

在shiro配置类中添加shiro标识解析bean:

/**
 * 配置页面的shiro标识的解析bean
 */
@Bean
public ShiroDialect shiroDialect() {
    return new ShiroDialect();
}

3.3 Shiro授权

首先,重写MyRealm中的授权方法:

/*重写授权方法*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    // 获取当前认证用户的用户名
    String uname = (String)principalCollection.getPrimaryPrincipal();
    // 从数据库查询当前用户的权限信息
    List permissionList = userService.getPermissions(uname);
    // 从数据库查询当前用户的角色信息
    List roleList = userService.getRoles(uname);
    // 将查询到的权限、角色信息给Shiro
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.addStringPermissions(permissionList);
    info.addRoles(roleList);
    return info;
}

然后,添加相应的两个查询方法,此处代码省略

最后添加鉴权失败后,进行异常拦截的通知类和方法:
新建ExpController类:

@ControllerAdvice
public class ExpController {
    @ResponseBody
    @ExceptionHandler(UnauthorizedException.class)
    public String handleShiroException(Exception ex) {
        return "无权限";
    }
    @ResponseBody
    @ExceptionHandler(AuthorizationException.class)
    public String AuthorizationException(Exception ex) {
        return "权限认证失败";
    }
}

Step 4 Shiro整合EhCache

在授权过程中,我们会发现,Shiro每次都会去访问数据库,较为耗费资源,引入缓存即可解决问题,Shiro 支持很多第三方缓存工具。官方提供了 shiro-ehcache,实现了把 EHCache 当 做 Shiro 的缓存工具的解决方案。其中最好用的一个功能是就是缓存认证执行的 Realm 方 法,减少对数据库的访问。

4.1 添加依赖



    org.apache.shiro
    shiro-ehcache
    1.4.2


    commons-io
    commons-io
    2.6

4.2 编写ehcache缓存配置

在resources下新建ehcache/ehcache-shiro.xml:



    
    
    
    
    
    
    

4.3 修改配置文件shiroconfig

在ShiroConfig类的setSecurityManager方法中加入如下代码:


image

getCacheManager()方法具体代码:

/**
 * 设置shiro的CacheManager
 */
private CacheManager getCacheManager() {
    // 1.实例化 Shiro 自身的 CacheManager,EhCache 的实现类
    EhCacheManager shiroCacheManager = new EhCacheManager();
    // 2.获取 EhCache 的配置类文件并转成输入流
    InputStream is = null;
    try {
        is = ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml");
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 3.实例化 EhCacheManager 自身对象
    net.sf.ehcache.CacheManager ehCacheManager =new  net.sf.ehcache.CacheManager(is);
    // 4.将 EhCacheManager 自身对象赋值给 Shiro 的 CacheManager
    shiroCacheManager.setCacheManager(ehCacheManager);
    // 5.返回
    return shiroCacheManager;
}

Step 5 实现多次输错密码锁定账号

5.1 在ehcache-shiro.xml中配置缓存策略




5.2 创建自定义凭证匹配器

创建凭证匹配器RetryLimitHashedCredentialsMatcher继承HashedCredentialsMatcher:

@Component
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
    // 声明缓存对象
    private Ehcache passwordRetryCache;
    // 获取EhCache缓存管理器并获取缓存策略
    public RetryLimitHashedCredentialsMatcher(EhCacheManager ehCacheManager) {
        this.passwordRetryCache = ehCacheManager.getCacheManager().getCache("loginRecordCache");
    }
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        // 声明变量记录错误次数
        int i = 0;
        // 1. 获取用户登录次数的缓存信息
        // 获取用户的身份信息(身份信息为缓存数据的键名)
        String username = token.getPrincipal().toString();
        // 获取缓存对象
        Element element = passwordRetryCache.get(username);

        // 判断是否有缓存数据
        if (element==null) {
            // 没有缓存数据,新建
            Element ele = new Element(username,new AtomicInteger(0));
            passwordRetryCache.put(ele);
        } else {
            // 有缓存,提取自增
            AtomicInteger atomicInteger = (AtomicInteger) element.getObjectValue();
            i = atomicInteger.incrementAndGet();
        }
        System.out.println("验证次数:"+i);
        // 3. 判断i次数
        if (i >= 4) {
            throw new ExcessiveAttemptsException();
        }

        // 4. 进行本次登录判断
        boolean match = super.doCredentialsMatch(token,info);

        // 5. 如果登录成功,则移除登录记录
        if (match) {
            passwordRetryCache.remove(username);
        }
        return match;
    }
}

5.3 修改配置类ShiroConfig

  • 将EhCacheManager的实例化交给Spring容器管理
    在获取EhCacheManager的getCacheManager方法前添加@Bean注解:


    image

    把此对象的实例化交给Spring容器托管,以便RetryLimitHashedCredentialsMatcher构造器使用

  • 修改密码匹配器:
    使用在ShiroConfig的设置SecurityManager方法中把原来默认的密码匹配器替换为新自定义的类:


    image

5.4 修改登录单元方法

image

至此,即完成功能开发。
详细代码请详见个人gitee仓库:https://gitee.com/chenpingcloud/git-repository

你可能感兴趣的:(Spring Boot 整合SSM+Thymeleaf+Shiro+EhCache实现登录认证和授权功能)