【Shiro】4.Spring Boot整合Shiro

整合思路

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

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

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

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

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

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

20210928170338.png

整合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。

过滤器缩写过滤器功能
anonAnonymousFilter指定URL可以匿名访问,无需认证和授权
authcFormAuthenticationFilter指定URL需要form表单登录,默认会从请求中获取username,password , rememberMe等参数并尝试登录,如果登录不了就会跳转到setLoginUrl配置的认证路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,因为可以定制出错返回的信息。
authcBasicBasicHttpAuthenticationFilter指定URL需要basic登录
logoutLogoutFilter登出过滤器,配置指定URL就可以实现退出功能,非常方便
noSessionCreationNoSessionCreationFilter禁止创建会话
permsPermissionsAuthorizationFilter需要指定权限才能访问
portPortFilter需要指定端口才能访问
restHttpMethodPermissionFilter将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
rolesRolesAuthorizationFilter需要指定角色才能访问
sslSslFilter需要https请求才能访问
userUserFilter需要已登录或 "记住我" 的用户才能访问

整合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)—> 资源

20210929162150.png

  • 设计用户表

    20210929182304.png

  • 设计角色表

    表结构

    20210929175606.png

    数据案例

    idrole
    1admin
    2user
  • 设计权限表

    表结构

    20210929175819.png

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

    数据案例

    idpermissionurl
    1user:*:*
    2user:find:1
  • 设计用户-角色表

    表结构

    20210929180038.png

  • 设计角色-权限表

    表结构

    20210929180350.png

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。

  2. 在User类中添加角色集合

    @Data
    public class User implements Serializable {
    
        private String id;
    
        private String username;
    
        private String password;
    
        private String salt;
    
        private List<String> roles;
    
    }
    
  3. 在Role类中添加权限集合

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

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

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

    List<Permission> getPermissionsByRoleId(String roleId);
    

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

  6. 整合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;
        }
    
    }
  7. Controller接口根据角色验证

    @GetMapping("/findAllByRole")
    @RequiresRoles({"admin", "user"})
    public String findAllByRole() {
        System.out.println("具有 admin 和 user 角色");
        return "具有 admin 和 user 角色";
    }
  8. Controller接口根据权限验证

    @GetMapping("/findAllByPermission")
    @RequiresPermissions(value = {"user:*:*", "user:find:1"})
    public String findAllByPermission() {
        System.out.println("具有 user:*:* 和 product:*:* 权限");
        return "具有 user:*:* 和 product:*:* 权限";
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值