虽然都2020年了,但我确实还不大会用shiro,所以利用空闲时间学习了一下基础的用法,参考网上的资料,总结一下自己的理解,也方便自己偶尔看看0.0。

Shiro是什么?

Shiro是一个轻量级的安全认证框架,他可以完成认证,授权,会话管理,缓存等一系列功能。

这是从百度百科kiang来的结构图:

springboot写入iot springboot loki_User

我个人对于这些结构及概念性的文字很头疼,一看就容易云里雾里,但是概念确实还是非常重要的,所以我大部分时候还是结合着代码理解概念。

怎么操作?(springboot整合Shiro)

  1. 首先,创建一个普通的springboot项目,除springboot之外的依赖
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>

我这里用了lombok插件,可以理解成是一个使用注解给实体类生成get/set,toString。。这些方法的插件,很好用。

  1. 三个实体类
    @Data 会生成各属性get/set,重写equals(),hashCode(),toString()方法
    @AllArgsConstructor 会生成全参构造方法
    @NoArgsConstructor 即为无参构造方法
    User.java(用户信息)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private int id;
    private String username;
    private String password;
    private Set<Role> roles;
}

Role.java (角色)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {

    private int id;
    private String roleName;
    private Set<Permission> permissions;
}

Permission.java (权限)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Permission {

    private int id;
    private String permissionName;

}
  1. 草率的业务实现类
    假装我们是从持久层拿到的数据,我一共准备了两个用户(zhangsan,lisi),两个角色(admin,user),两个权限字符(query,add)。
    zhangsan 为 admin 拥有 query,add 两个权限。
    lisi 为 user 拥有 query 权限。
@Service
public class LoginServiceImpl implements LoginService {

    /**
     * 模拟数据库查询用户信息
     *
     * @param name
     * @return
     */
    @Override
    public User getUserByName(String name) {
        //一共两个权限 query和add 管理员拥有两个权限 用户只拥有query
        Permission p1 = new Permission(1,"query");
        Permission p2 = new Permission(2,"add");
        Set<Permission> ap = new HashSet<>();
        ap.add(p1);
        ap.add(p2);
        Set<Permission> up = new HashSet<>();
        up.add(p1);

        Role r1 = new Role(1,"admin",ap);
        Role r2 = new Role(2,"user",up);
        Set<Role> ar = new HashSet<>();
        ar.add(r1);
        Set<Role> ur = new HashSet<>();
        ur.add(r2);

        //张三为管理员角色 李四为普通用户角色
        User u1 = new User(1,"zhangsan","123456",ar);
        User u2 = new User(2,"lisi","123456",ur);

        Map<String,User> map = new HashMap<>();
        map.put("zhangsan",u1);
        map.put("lisi",u2);

        return map.get(name);
    }
}
  1. 自定义Realm用于查询用户的角色和授权信息
    需要继承 AuthorizingRealm 并重写他的两个方法,上面的是授权,下面的是登录认证。
/**
 * 自定义Realm用于查询用户的角色和权限信息并保存到权限管理器
 */
public class CustomRealm extends AuthorizingRealm {
    
    @Autowired
    LoginService loginService;

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取用户名
        String name = (String) principalCollection.getPrimaryPrincipal();
        //获取用户
        User user = loginService.getUserByName(name);
        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role r : user.getRoles()) {
            //添加角色
            simpleAuthorizationInfo.addRole(r.getRoleName());
            //添加权限
            for (Permission p : r.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(p.getPermissionName());
            }
        }
        return simpleAuthorizationInfo;
    }

    /**
     * 登录验证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (null == authenticationToken.getPrincipal()) return null;
        //获取用户
        String name = authenticationToken.getPrincipal().toString();
        User user = loginService.getUserByName(name);
        if (null == user) return null;

        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword(), getName());
        return simpleAuthenticationInfo;
    }
}
  1. 把一系列东西放入spring容器
    把自定义的Realm和实现记住我功能的 cookieRememberMeManager 放进 SecurityManager,再把 SecurityManager 放入spring容器。
    再配置Shiro的过滤器。
    tips:map.put("/**",“user”); 这个是指放行登录认证成功或记住我的用户。
/**
 * 把CustomRealm和SecurityManager等加入到spring容器
 */
@Configuration
public class ShiroConfig {

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAAPC = new DefaultAdvisorAutoProxyCreator();
        defaultAAPC.setProxyTargetClass(true);
        return defaultAAPC;
    }

    //自定义的验证方式
    @Bean
    public CustomRealm myShiroRealm(){
        CustomRealm cr = new CustomRealm();
        return cr;
    }

    //配置cookie基础属性
    public SimpleCookie simpleCookie(){
        SimpleCookie sc = new SimpleCookie("rememberMe");
        //cookie有效时间 单位秒 以下设置了30天
        sc.setMaxAge(30 * 24 * 60 * 60);
        return sc;
    }

    //cookie管理器
    public CookieRememberMeManager cookieRememberMeManager(){
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(simpleCookie());
        // cookieRememberMeManager.setCipherKey用来设置加密的Key,参数类型byte[],字节数组长度要求16
        cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
        return cookieRememberMeManager;
    }

    //权限管理
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager dsm = new DefaultWebSecurityManager();
        //自定义验证方式
        dsm.setRealm(myShiroRealm());
        //记住我
        dsm.setRememberMeManager(cookieRememberMeManager());
        return dsm;
    }

    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String,String> map = new HashMap<>();
        //登出
        map.put("/logout", "logout");
        //对所有用户认证
        map.put("/**","user");
        //登录
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首页 登录成功跳转
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //验证失败跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}
  1. 登录控制器
    重点是学校Shiro,所以就不写页面了,直接返回字符串。
@RestController
public class LoginController {

    @RequestMapping("/login")
    public String login(String username, String password, boolean rememberMe) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken upt = new UsernamePasswordToken(username, password);

        try {
            upt.setRememberMe(rememberMe);
            subject.login(upt);
            //可以在逻辑中检查用户有没有相应角色或权限 没有的话会抛出AuthorizationException异常
//            subject.checkRole("admin");
//            subject.checkPermission("add");
        } catch (AuthenticationException e) {
            //不同的操作错误会抛出不同的异常
            e.printStackTrace();
            return "账号或密码错误";
        } catch (AuthorizationException e) {
            e.printStackTrace();
            return "无权限";
        }
        return "login success";
    }

    /**
     * 注解验证
     */
    @RequiresRoles("admin")
    @RequiresPermissions("add")
    @RequestMapping("/index")
    public String index() {
        return "index";
    }
}
  1. 由于注解方式不方便捕获异常,所以增加了一个类拦截异常
/**
 * 通过注解的方式无法捕捉到抛出的异常,也就没有办法很好的给前端反馈
 * 所以使用这个类拦截注解方式抛出的无权限异常
 *
 */
@ControllerAdvice
@Slf4j
public class MyExpectHandle {
    @ExceptionHandler
    @ResponseBody
    public String ErrorHandle(AuthorizationException e){
        log.error("未通过权限验证",e);
        return "使用注解方式拦截下的未通过权限验证异常";
    }
}
  1. 启动