整合思路

ShiroFilter会拦截所有请求,Shrio会判断哪些请求需要做认证和授权,哪些不需要做。

如果请求中访问的是系统的公共资源,则不需要进行认证和授权的操作,ShiroFilter直接放行即可。

如果请求中访问的是系统的受限资源,若第一次访问需要做认证,认证成功后,后续的访问进行授权。ShiroFilter依赖SecurityManager来完成认证和授权的具体操作,同时SecurityManager也依赖Realm来获取认证和授权的相应数据。

公共资源不需要认证和授权,任何用户都能访问。似于登录页面,注册页面。

受限资源是需要认证成功并赋予权限才能访问的资源。类似于系统主页,用户主页。

如果Shiro整合Spring Cloud,则将相应操作整合进Spring Cloud Gateway或者zuul即可。

shiro整合springcloud springcloud集成shiro_shiro整合springcloud

整合Shiro实现认证

1、pom.xml 中引入依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.5.3</version>
</dependency>

2、创建工厂工具类

@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;

    // 工厂就是该方法的参数,当Spring Boot启动时,该参数就会接收到工厂
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) 
                        throws BeansException {
        context = applicationContext;
    }

    // 根据Bean的名字获取工厂中指定对象
    public static Object getBean(String beanName) {
        return applicationContext.getBean(beanName);
    }

}

3、构建shiro包,在shiro包下构建realms包

4、realms包中构建自定义Realm

public class UserRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    // 认证操作
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取前端传入身份信息
        String username = (String) token.getPrincipal();

        // 从工厂中获取userService
        UserSerivce userSerivce = (UserSerivce) ApplicationContextUtils.getBean("userService");
        // 根据身份信息从DB中获取User
        User user = userSerivce.getUserByUsername(username);

        // 获取加密后的密码和Salt,Shiro自动进行认证
        if (user != null) {
            return new SimpleAuthenticationInfo(username, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
        }

        return null;
    }

}

默认被Spring工厂托管的Bean的名字都是其类名首字母小写,也可以指定,比方说@Service("userService")。 

5、创建Shiro配置类

@Configuration
public class ShiroConfig {

    // 创建ShiroFilter
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 给ShiroFilter注入SecurityManager
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        // 设置默认认证路径,认证失败后会调用该接口,也算是公共资源
        shiroFilterFactoryBean.setLoginUrl("/user/login");

        // 配置公共资源和受限资源
        Map<String, String> map = new HashMap<>();
        // anon是过滤器的一种,表示该资源是公共资源,需要设置在authc上面
        map.put("/user/register", "anon");
        map.put("/user/login", "anon");
        // authc是过滤器的一种,表示除了设置公共资源和默认认证路径之外所有资源是受限资源
        map.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        return shiroFilterFactoryBean;
    }

    // 创建具有Web特性的SecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getRealm") Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

        // 给SecurityManager注入Realm
        defaultWebSecurityManager.setRealm(realm);

        return defaultWebSecurityManager;
    }

    // 创建自定义Realm
    @Bean
    public Realm getRealm() {
        UserRealm userRealm = new UserRealm();

        // 设置Hash凭证校验匹配器,用来完成密码加密校验
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密算法MD5
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 设置散列次数1024
        hashedCredentialsMatcher.setHashIterations(1024);

        // 注入凭证校验匹配器
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher);

        return userRealm;
    }

}

6、设计数据库

user表中需要在基础上添加salt字段。 

7、创建随机生成Salt的工具类

public class SaltUtils {

    /**
     * 随机生成定长的Salt
     * @param n 长度
     * @return Salt
     */
    public static String getSalt(int n) {
        char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~!@#$%^&*()_+|{}:.,<>?/".toCharArray();
        
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0; i < n; i++) {
            char c = chars[new Random().nextInt(chars.length)];
            stringBuilder.append(c);
        }

        return stringBuilder.toString();
    }
    
}

8、创建Controller

默认DB、MP和Service都已经配置并编写完毕。

以下代码为了方便展示,将业务写在Controller中,实际开发时需要提取进Service。

@RestController
@RequestMapping("/user")
public class UserContoller {

    @Autowired
    private UserService userService;

    @PostMapping("register")
    public Response register(@RequestBody UserRegisterDto userRegisterDto) {
        try {
            // 生成8位Salt
            String salt = SaltUtils.getSalt(8);
            // MD5 + Hash + Salt给密码加密
            Md5Hash md5Hash = new Md5Hash(userRegisterDto.getPassword(), salt, 1024);
            // 注册
            userService.register(userRegisterDto.getUsername(), md5Hash.toHex(), salt);
            // 注册成功
            return Response.ok().message("注册成功");
        } catch (Exception e) {
            return Response.error(ResponseEnum.UNIFIED_ERROR).message("注册失败");
        }
    }

    @PostMapping("login")
    public Response login(@RequestBody UserLoginDto userLoginDto) {
        Subject subject = SecurityUtils.getSubject();

        try {
            // 登录,Shiro自动认证
            subject.login(new UsernamePasswordToken(userLoginDto.getUsername(), userLoginDto.getPassword()));
            // 认证成功
            return Response.ok().message("登录成功");
        } catch (UnknownAccountException e) {
            return Response.error(ResponseEnum.UNKNOWN_ACCOUNT_ERROR);
        } catch (IncorrectCredentialsException e) {
            return Response.error(ResponseEnum.INCORRECT_CREDENTIALS_ERROR);
        }
    }

    @GetMapping("logout")
    public Response login() {
        Subject subject = SecurityUtils.getSubject();

        subject.logout();

        return Response.ok().message("退出成功");
    }

}

在Web环境中,只要Shiro配置类中配置了SecurityManager,那么Spring就会将其托管,无需在Controller中单独创建。

 

Shiro过滤器

Shiro提供多个默认的过滤器,我们可以用这些过滤器来配置控制指定URL的权限。

常用的有两种:anon和authc。

过滤器缩写

过滤器

功能

anon

AnonymousFilter

指定URL可以匿名访问,无需认证和授权

authc

FormAuthenticationFilter

指定URL需要form表单登录,默认会从请求中获取username,password , rememberMe等参数并尝试登录,如果登录不了就会跳转到setLoginUrl配置的认证路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,因为可以定制出错返回的信息。

authcBasic

BasicHttpAuthenticationFilter

指定URL需要basic登录

logout

LogoutFilter

登出过滤器,配置指定URL就可以实现退出功能,非常方便

noSessionCreation

NoSessionCreationFilter

禁止创建会话

perms

PermissionsAuthorizationFilter

需要指定权限才能访问

port

PortFilter

需要指定端口才能访问

rest

HttpMethodPermissionFilter

将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释

roles

RolesAuthorizationFilter

需要指定角色才能访问

ssl

SslFilter

需要https请求才能访问

user

UserFilter

需要已登录或 "记住我" 的用户才能访问

整合Shiro实现授权

前文说了,Shiro提供了三种授权方式,在前后端分离的系统中,我们主要使用注解式实现授权。后端只负责写接口传递用户的权限信息,具体前台如何显示由前端负责。

1. @RequiresRoles注解

该注解标注在接口方法上,表示是指定的角色才可以访问该接口。

@GetMapping("/findByRole")
@RequiresRoles("admin")
public String findByRole() {
    ...
}

也可以设置多个角色,表示同时具有指定的所有角色才能访问该接口。

@GetMapping("/findAllByRole")
@RequiresRoles({"admin", "user"})
public String findAllByRole() {
    ...
}

2. @RequiresPermissions注解

该注解标注在接口方法上,表示有指定访问权限才可以访问该接口。

@GetMapping("/findByPermission")
@RequiresPermissions("user:*:*")
public String findByPermission() {
    ...
}

也可以设置多个访问权限,表示同时具有指定的所有访问权限才能访问该接口。

@GetMapping("/findAllByPermission")
@RequiresPermissions(value = {"user:*:*", "user:find:1"})
public String findAllByPermission() {
    ...
}

3. 授权数据持久化

在实际项目中,权限数据需要在DB中获,因此我们要设计角色表权限表

通常情况下,一般是这样设计的:用户 <—(* *)—> 角色,角色 <—(* *)—> 权限,权限 <—(1 1)—> 资源

shiro整合springcloud springcloud集成shiro_shiro_02

  • 设计用户表
  • 设计角色表
    表结构

数据案例

id

role

1

admin

2

user

  • 设计权限表
    表结构

permission为权限标识符,url为权限标识符对应的URL。

数据案例

id

permission

url

1

user:*:*

2

user:find:1

  • 设计用户-角色表
    表结构
  • 设计角色-权限表
    表结构

4. 授权流程

  1. 构建Role和Permission的Bean
@Data
public class Role implements Serializable {
    
    private String id;
    
    private String role;
    
}
@Data
public class Permission implements Serializable {

    private String id;

    private String permission;

    private String url;

}

所有的Bean必须序列化,因为后文要将该Bean存入Redis。

  1. 在User类中添加角色集合
@Data
public class User implements Serializable {

    private String id;

    private String username;

    private String password;

    private String salt;

    private List<String> roles;

}
  1. 在Role类中添加权限集合
@Data
public class Role implements Serializable {
    
    private String id;
    
    private String role;
    
    List<Permission> permissions;
    
}
  1. 在UserMapper和UserService中提供通过username查询单个用户的角色集合的接口

具体使用逻辑实现使用MP或者Mybatis不同,自行选择即可。

List<Role> getRolesByUsername(String username);
  1. 在UserMapper和UserService中提供通过role_id查询单个角色的权限集合的接口
List<Permission> getPermissionsByRoleId(String roleId);

具体使用逻辑实现使用MP或者Mybatis不同,自行选择即可。

  1. 整合Realm中授权的方法
public class UserRealm extends AuthorizingRealm {

    // 授权操作
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取身份信息
        String username = (String) principals.getPrimaryPrincipal();

        // 从工厂中取出UserService
        UserService userService = (UserService) ApplicationContextUtils.getBean("userService");

        // 注入该角色的角色和权限
        List<Role> roles = userService.getRolesByUsername(username);
        if (roles != null) {
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            roles.forEach(role -> {
                // 注入角色
                simpleAuthorizationInfo.addRole(role.getRole());

                // 获取权限集合
                List<Permission> permissions = userService.getPermissionsByRoleId(role.getId());
                // 也可以使用该方法判断集合是否不为空
                if (!CollectionUtils.isEmpty(permissions)) {
                    permissions.forEach(permission -> {
                        // 注入权限
                        simpleAuthorizationInfo.addStringPermission(permission.getPermission());
                    });
                }
            });
            return simpleAuthorizationInfo;
        }
        
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取前端传入身份信息
        String username = (String) token.getPrincipal();

        // 获取userService
        UserSerivce userSerivce = (UserSerivce) ApplicationContextUtils.getBean("userService");
        // 根据身份信息从DB中获取User
        User user = userSerivce.getUserByUsername(username);

        // 获取加密的密码和Salt,Shiro自动进行认证
        if (user != null) {
            return new SimpleAuthenticationInfo(username, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
        }

        return null;
    }

}
  1. Controller接口根据角色验证
@GetMapping("/findAllByRole")
@RequiresRoles({"admin", "user"})
public String findAllByRole() {
    System.out.println("具有 admin 和 user 角色");
    return "具有 admin 和 user 角色";
}
  1. Controller接口根据权限验证
@GetMapping("/findAllByPermission")
@RequiresPermissions(value = {"user:*:*", "user:find:1"})
public String findAllByPermission() {
    System.out.println("具有 user:*:* 和 product:*:* 权限");
    return "具有 user:*:* 和 product:*:* 权限";
}