全栈小白的gravatar头像
全栈小白 2022-12-12 15:35:45
SpringBoot整合Shiro前后端不分离

原创声明:本人所发内容及涉及源码,均为亲手所撸,如总结内容有误,欢迎指出

一、组件说明

SpringBoot整合Shiro前后端不分离

来源于百度图片,三个部分,主题Subject、安全管理器SecurityManager、Realm

1.2 Shiro配置的几个过滤器

anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
authcBearer(BearerHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class),
invalidRequest(InvalidRequestFilter.class);

二、整合步骤

2.1 导入依赖

<dependencies>
<!--        web启动依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        thymeleaf依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
<!--        lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
<!--        mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
<!--        mp-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.0</version>
        </dependency>
<!--        shiro依赖-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.8.0</version>
        </dependency>
    </dependencies>

2.2 配置文件

server:
  port: 2022spring:
  # thymeleaf配置
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    encoding: UTF-8
    cache: false
  # 数据源配置
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///shiro-spring-boot-demo?characterEncoding=utf-8&useSSL=false
    username: root
    password: root
# mp的配置
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  type-aliases-package: com.cxs.model
  mapper-locations: classpath:mapper/*.xml
  global-config:
    banner: false

2.3 编写认证Realm

AuthorizingRealm有两个方法需要我们重写

  • doGetAuthenticationInfo:自定义认证

    根据用户输入的用户名查数据库,存在将其封装为一个AuthenticationInfo对象返回,不存在自己处理,这里抛出UnknownAccountException异常,这个异常会在异常处理器处理(后面提)

    AuthenticationInfo对象中会有一个载荷,实现类SimpleAuthenticationInfo构造方法中的第一个参数,这个载荷用于认证成功后Shiro存储的实体,可以自定义,我这直接将用户信息存里面了,但是存什么,在 subject.getPrincipal()中取出来的就是什么,建议将用户id和用户名存一下

  • doGetAuthorizationInfo:自定义授权

    根据认证成功后Shiro存的载荷查询用户应有的角色权限,封装到一个AuthorizationInfo对象中,由Shiro的authc过滤器去判断是否有权限,看下其继承结构

    SpringBoot整合Shiro前后端不分离

/*
 * @Project:spring-boot-shiro-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
public class AuthRealm extends AuthorizingRealm {
​
    @Autowired
    private UserService userService;
​
    public AuthRealm(){
        // 注入密码加密
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashIterations(1024);
        matcher.setHashAlgorithmName("MD5");
        this.setCredentialsMatcher(matcher);
    }
​
    public static void main(String[] args) {
        Md5Hash md5Hash = new Md5Hash("user","user",1024);
        System.out.println(md5Hash.toString());
    }
​
    /**
     * 负责认证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        if (token == null || token.getPrincipal() == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        // 从 AuthenticationToken 中获取当前用户
        String username = (String) token.getPrincipal();
        // 查询数据库获取用户信息,此处使用 Map 来模拟数据库
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userService.getOne(wrapper);
​
        // 用户不存在
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        /**
         * 将获取到的用户数据封装成 AuthenticationInfo 对象返回,此处封装为 SimpleAuthenticationInfo 对象。
         *  参数1. 认证的实体信息,可以是从数据库中获取到的用户实体类对象或者用户名
         *  参数2. 查询获取到的登录密码
         *  参数3. 盐值
         *  参数4. 当前 Realm 对象的名称,直接调用父类的 getName() 方法即可
         */
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
        // 设置盐
        info.setCredentialsSalt(ByteSource.Util.bytes(user.getUserName()));
        return info;
    }
​
    /**
     * 负责权限
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Object principal = principals.getPrimaryPrincipal();
        User token = (User) principal;
        // 这个步骤在认证之后,principal必定不为空
        // 根据用户获取对应权限,这里就不获取了,因为权限和用户在一张表里
        Set<String> roles = new HashSet<>();
        roles.add(token.getRole());
        AuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        return info;
    }
}

2.4 编写Shiro核心配置类

以下两步配置后不会发生无限重定向,去他的不说了,注释都写了

  • shiroFilterFactoryBean.setLoginUrl("/login"); 配置登录页面的地址,

  • 放行/login路径

/*
 * @Project:spring-boot-shiro-demo
 * @Author:cxs
 * @Motto:放下杂念,只为迎接明天更好的自己
 * */
@Configuration
public class ShiroConfig {
​
    /**
     * 注入realm
     * @return
     */
    @Bean
    public AuthRealm authRealm(){
        return new AuthRealm();
    }
    /**
     * 配置 SecurityManager
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置Realm
        securityManager.setRealm(authRealm());
        return securityManager;
    }
​
    /**
     * 配置访问资源需要的权限
     */
    @Bean
    ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 指定登录的地址,请勿指定xxx.html,说过了templates下的文件不能通过浏览器直接访问
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 自定义过滤器
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        filterChainDefinitionMap.put("/error", "anon");
        filterChainDefinitionMap.put("/login", "anon"); 
        filterChainDefinitionMap.put("/auth/login", "anon"); // anno可匿名访问
        filterChainDefinitionMap.put("/**", "authc"); // 其他所有资源均需登录才能访问
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
​
    //开启对shior注解的支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager());
        return advisor;
    }
}

2.5 编写接口

登陆接口,

User user = (User) subject.getPrincipal();

这里取出来的值是realm中决定的,不一致或发生类型转换异常(ClassCastException)

/**
     * 用户认证接口
     * @param request
     * @param model
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/auth/login")
    public String authLogin(HttpServletRequest request, Model model, String username, String password){
        if (!StringUtils.hasLength(username)) {
            model.addAttribute("msg", "用户名不能为空");
            return "login";
        }
        if (!StringUtils.hasLength(password)) {
            model.addAttribute("msg", "密码不能为空");
            return "login";
        }
        // 获取当前用户主体
        Subject subject = SecurityUtils.getSubject();
        // 将用户名和密码封装成 UsernamePasswordToken 对象
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username.trim(), password.trim());
        // 执行认证流程,走我们的自定义realm
        subject.login(usernamePasswordToken);
        // 登录成功,将用户信息存于session
        HttpSession session = request.getSession();
        // 这里获取的载荷是realm中决定的
        User user = (User) subject.getPrincipal();
        session.setAttribute("user", user);
        return "index";
    }

一个列表接口,这个接口只有admin角色才能访问,有两个用户

admin拥有admin角色,user拥有user角色,数据库sql文件中内置了

@GetMapping("/list")
@RequiresRoles("admin")
public String list(Model model){
    model.addAttribute("list", userService.list());
    return "list";
}

2.6 统一的异常处理器

为了错误稍微的好看些,配置个异常处理器,主要处理两个异常,一个认证、一个授权,有需要可以自己加

新建了一个error页面,来显示错误

/*
* @Project:spring-boot-shiro-demo
* @Author:cxs
* @Motto:放下杂念,只为迎接明天更好的自己
* */
@ControllerAdvice
public class ShiroExceptionHandler {
​
    /**
     * 处理用户认证时所抛出的异常
     * @param model
     * @param e
     * @return
     */
    @ExceptionHandler(value = AuthenticationException.class)
    public String authExceptionHandle(Model model, AuthenticationException e){
        if (e instanceof UnknownAccountException) {
            // 用户名不存在会抛出这个异常
            model.addAttribute("msg", "用户名不存在");
            return "error";
        } else if (e instanceof CredentialsException) {
            // 密码验证失败会抛出这个异常
            model.addAttribute("msg", "密码验证失败");
            return "error";
        } else {
            model.addAttribute("msg", "用户名或密码错误");
            return "error";
        }
    }
​
    /**
     * 处理授权时抛出的异常
     * @param model
     * @param e
     * @return
     * AuthorizationException 用户无权限访问资源会抛出这个异常
     */
    @ExceptionHandler(value = AuthorizationException.class)
    public String forbiddenExceptionHandle(Model model, AuthorizationException e){
        model.addAttribute("msg", "用户权限不足,拒绝访问");
        return "error";
    }
}

三、成果展示

3.1 admin/admin登录

SpringBoot整合Shiro前后端不分离

3.2 admin查看用户列表

SpringBoot整合Shiro前后端不分离

3.3 user/user登录

SpringBoot整合Shiro前后端不分离

3.4 user查看用户列表

SpringBoot整合Shiro前后端不分离

3.5 用户名或密码错误

SpringBoot整合Shiro前后端不分离

四、源代码地址

码云:https://gitee.com/whole-stack-of-white/shiro-spring-boot-demo


打赏
最近浏览
暂无贡献等级
stedian  LV4 2023年12月20日
wuying8208  LV15 2023年12月2日
newhaijun  LV15 2023年3月8日
随便取个名字_哈哈  LV27 2023年3月6日
hbsoft2008  LV16 2023年3月2日
Iterman  LV2 2023年2月28日
天上飞的菜鸟 2023年2月22日
暂无贡献等级
BestClever  LV32 2023年2月15日
EvilCCCC  LV1 2023年2月8日
顶部 客服 微信二维码 底部
>扫描二维码关注最代码为好友扫描二维码关注最代码为好友