前后端分离时后端shiro权限认证

简述:

shiro是根据sessionID来识别是不是同一个request,但如果前后分离的话,就会出现跨域的问题,session很可能就会发生变化,这样就需要用一个标记来表明是同一个请求

1.shiro有三大组件

1.1 Subject

代表当前与程序进行交互的使用者

1.2 SecurityManager

是 Shiro 架构的心脏,并作为一种“保护伞”对象来协调内部的安全组件共同构成一个对象图

1.3 Realms

一个用于认证和授权的类

2.首先前后端分离会有跨域问题,还有复杂请求问题

2.1跨域如下

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * 目的: 跨域访问控制
 *          做前后分离的话,这个也是必配的
 * 备注说明:
 */
@Configuration
public class CorsConfig {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许任何域名使用
        corsConfiguration.addAllowedOrigin("*");
        // 允许任何头
        corsConfiguration.addAllowedHeader("*");
        // 允许任何方法(post、get等)
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }


    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 对接口配置跨域设置
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}

2.2复杂请求如下

package com.example.demo;

import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * 目的: 过滤OPTIONS请求
 *      继承shiro 的form表单过滤器,对 OPTIONS 请求进行过滤。
 *      前后端分离项目中,由于跨域,会导致复杂请求,即会发送preflighted request,这样会导致在GET/POST等请求之前会先发一个OPTIONS请求,但OPTIONS请求并不带shiro
 *      的'authToken'字段(shiro的SessionId),即OPTIONS请求不能通过shiro验证,会返回未认证的信息。
 *
 * 备注说明: 需要在 shiroConfig 进行注册
 */
public class CORSAuthenticationFilter extends FormAuthenticationFilter {

    /**
     * 直接过滤可以访问的请求类型
     */
    private static final String REQUET_TYPE = "OPTIONS";


    public CORSAuthenticationFilter() {
        super();
    }


    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (((HttpServletRequest) request).getMethod().toUpperCase().equals(REQUET_TYPE)) {
            return true;
        }
        return super.isAccessAllowed(request, response, mappedValue);
    }


    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse res = (HttpServletResponse)response;
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setStatus(HttpServletResponse.SC_OK);
        res.setCharacterEncoding("UTF-8");
        PrintWriter writer = res.getWriter();

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("msg","请先登录");
        writer.write(String.valueOf(jsonObject));
        writer.close();
        return false;
    }
}

2.3先配置好跨域问题后开始shiro配置

2.3.1第一步

要进行认证那么必有一张表用于保存登录信息如下
import lombok.Data;

@Data
@Entity
@Table
public class User {
    private String username;
    private String password;

}

这张表是一个数据库实体,在登录认证时查询数据库比对传入的username 和password是否正确

2.3.2第二步

创建MyRealm类用于登录认证和授权
package com.example.demo;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class MyRealm extends AuthorizingRealm {
//    授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        return null;
    }
//    认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        这里没有查数据库
        在实际项目中需要查数据进行验证
        User user = new User();
        user.setPassword("123");
        user.setUsername("123");
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        if (user == null){
            throw new UnknownAccountException("没有该用户");
        }
        if (!token.getUsername().equals(user.getUsername()) || !token.getPassword().equals(user.getPassword())){
            throw new IncorrectCredentialsException("用户名或密码不正确") ;
        }

        return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
    }
}

2.3.3第三步

配置自定义的session管理器
package com.example.demo;


import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * 目的: shiro 的 session 管理
 *      自定义session规则,实现前后分离,在跨域等情况下使用token 方式进行登录验证才需要,否则没必须使用本类。
 *      shiro默认使用 ServletContainerSessionManager 来做 session 管理,它是依赖于浏览器的 cookie 来维护 session 的,
 *      调用 storeSessionId  方法保存sesionId 到 cookie中
 *      为了支持无状态会话,我们就需要继承 DefaultWebSessionManager
 *      自定义生成sessionId 则要实现 SessionIdGenerator
 *
 * @author 小鸟的胖次
 */
public class ShiroSession extends DefaultWebSessionManager {
    /**
     * 定义的请求头中使用的标记key,用来传递 token
     */
    private static final String AUTH_TOKEN = "authToken";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";


    public ShiroSession() {
        super();
        //设置 shiro session 失效时间,默认为30分钟,这里现在设置为15分钟
        //setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15);
    }



    /**
     * 获取sessionId,原本是根据sessionKey来获取一个sessionId
     * 重写的部分多了一个把获取到的token设置到request的部分。这是因为app调用登陆接口的时候,是没有token的,登陆成功后,产生了token,我们把它放到request中,返回结
     * 果给客户端的时候,把它从request中取出来,并且传递给客户端,客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了
     * @param request ServletRequest
     * @param response ServletResponse
     * @return Serializable
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //获取请求头中的 AUTH_TOKEN 的值,如果请求头中有 AUTH_TOKEN 则其值为sessionId。shiro就是通过sessionId 来控制的
        String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN);

        if (StringUtils.isEmpty(sessionId)){
            //如果没有携带id参数则按照父类的方式在cookie进行获取sessionId
            return super.getSessionId(request, response);

        } else {
            //请求头中如果有 authToken, 则其值为sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            //sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        }
    }


}

2.3.4第四步 重点!!!!!!!!!

配置shiro

创建ShiroConfig类

package com.example.demo;

import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 描述 shiro的配置类
 *
 * @author 小鸟的胖次
 */
@Configuration
public class ShiroConfig {

    /**
     * 对shiro的拦截器进行注入
     * <p>
     * securityManager:
     * 所有Subject 实例都必须绑定到一个SecurityManager上,SecurityManager 是 Shiro的核心,初始化时协调各个模块运行。然而,一旦 SecurityManager协调完毕,
     * SecurityManager 会被单独留下,且我们只需要去操作Subject即可,无需操作SecurityManager 。 但是我们得知道,当我们正与一个 Subject 进行交互时,实质上是
     * SecurityManager在处理 Subject 安全操作
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        
        Map<String, Filter> customFilterMap = new LinkedHashMap<>();
        customFilterMap.put("corsAuthenticationFilter", new CORSAuthenticationFilter());
        shiroFilter.setFilters(customFilterMap);
        
        //拦截器,配置访问权限 必须是LinkedHashMap,因为它必须保证有序。滤链定义,从上向下顺序执行,一般将 /**放在最为下边
        Map<String, String> filterMap = new LinkedHashMap<String, String>();

        filterMap.put("/login", "anon");
        //剩余的请求shiro都拦截
        filterMap.put("/**/*", "authc");

        shiroFilter.setFilterChainDefinitionMap(filterMap);
		// 配置复杂请求过滤器

        return shiroFilter;
    }
    /**
     * securityManager 核心配置
     * 安全控制层
     * @return
     */
    @Bean
    public org.apache.shiro.mgt.SecurityManager securityManager(){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //设置自定义的realm
        defaultWebSecurityManager.setRealm(myRealm());
        //自定义的shiro session 缓存管理器
        defaultWebSecurityManager.setSessionManager(sessionManager());

        return defaultWebSecurityManager;
    }


    /**
     * 自定义的realm
     * @return
     */
    @Bean
    public MyRealm myRealm() {
        return new MyRealm();
    }


    /**
     * 开启shiro 的AOP注解支持
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 自定义的 shiro session 缓存管理器,用于跨域等情况下使用 token 进行验证,不依赖于sessionId
     * @return
     */
    @Bean
    public SessionManager sessionManager(){
        //将我们继承后重写的shiro session 注册
        ShiroSession shiroSession = new ShiroSession();
        //如果后续考虑多tomcat部署应用,可以使用shiro-redis开源插件来做session 的控制,或者nginx 的负载均衡
 
        shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO());
        return shiroSession;
    }
}

3.创建Controller进行测试

package com.example.demo;

import org.apache.catalina.security.SecurityUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;

import java.io.Serializable;

/**
 * @author 小鸟的胖次
 */
@RequestMapping("/")
@org.springframework.stereotype.Controller
public class Controller {

    @GetMapping("/user")
    @ResponseBody
    public String user (){
        Subject subject = SecurityUtils.getSubject();
        User user = (User) subject.getPrincipal();
        System.out.println("当前操作者username:" + user.getUsername());
        return "成功访问到user请求";
    }
    @PostMapping("/login")
    @ResponseBody
    public String login (String username , String password){
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        try {
            SecurityUtils.getSubject().login(token);
        } catch (AuthenticationException e) {
            e.printStackTrace();
        }

        Subject subject = SecurityUtils.getSubject();
        Serializable tokenId = subject.getSession().getId();

        return String.valueOf(tokenId);
    }
    @GetMapping("/list")
    @ResponseBody
    public String list (){
        Subject subject = SecurityUtils.getSubject();
        User user = (User) subject.getPrincipal();
        System.out.println("当前操作者username:" + user.getUsername());
        return "成功访问到list请求";
    }
}

4.测试结果

4.1先不登录访问user

在这里插入图片描述

登录失败,后端将我们的请求重定向到login.jsp页面但是我们没有写 所以有404错误

4.2先在login接口登录

在这里插入图片描述

返回了一串字符串 这就是后端传回来的token 以后的全部请求都要携带这个参数

4.3携带4.2传回来的token访问user和list

4.3.1 访问user

在这里插入图片描述

4.3.1 访问list

在这里插入图片描述

5.后台控制台

可以看到是谁登录
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值