zoukankan      html  css  js  c++  java
  • 安全框架--shiro

    安全框架--shiro

    0.2 名词及含义

    SecurityManager:安全管理器,由框架提供的,整个shiro框架最核心的组件。

    Realm:安全数据桥,类似于项目中的DAO,访问安全数据的,框架提供,开发人员也可自己编写

    0.3 网上关于shiro的资料

    https://www.2cto.com/kf/201604/502563.html

    https://blog.csdn.net/m0_38053538/article/details/80965359

    1.前后端分离的登陆

    shiro总结:

    1.shiro登录

    前台登录 --> loginController --> 完成登录认证 --> token(jsessionid)带到前端去 --> 每次发送请求过来(jsessionid带过来) --> 登录认证过滤器 --> shiro重写getSession的方法 --> 访问数据

    2.shiro授权

    前台登录 --> 授权过滤器(处理没有权限的时候,返回json格式) --> map(查询数据库权限交给shiro管理) --> 判断当前用户操作数据是否有权限(realm) --> 如果查询出来没有权限 --> 提示没有权限

    1.1 shiro概念回顾

    shiro是安全的框架,轻量级,Apache公司的,它具备身份认证 授权,密码学和会话管理

    spring security也是安全框架, 重量级 (可以和spring很好融合),spring公司的

    1.2 业务流程

    LoginController层:

    1.获取subject(主体)

    2.判断主体是否认证通过

    认证过:可以访问

    认证不过:去认证

    通过UserNameAndPassWordToken 去获得: token(令牌)

    调用subject.login(token),去完成认证

    3.调用到对应realm

    取出数据库密码

    把用户名和密码交给shiro ,去认证

    4.把信息保存session里面

    5.执行其他操作

    1.2.1 搭建shiro环境

    1.引入分层依赖

    shiro层(因为需要查询数据库)

    <dependency>
    <groupId>cn.itsource</groupId>
    <artifactId>crm_service</artifactId>
    <version>1.0-SNAPSHOT</version>
    </dependency>

    web.controller层:也需要shiro的依赖

    <dependency>
    <groupId>cn.itsource</groupId>
    <artifactId>crm_shiro</artifactId>
    <version>1.0-SNAPSHOT</version>
    </dependency>
    2.导入shiro需要的jar包:shiro模块下,pom.xml
            <!--shiro的jar包-->
    <dependency>
               <groupId>org.apache.shiro</groupId>
               <artifactId>shiro-all</artifactId>
               <version>1.4.1</version>
           </dependency>

           <!--shiro的依赖包-->
           <dependency>
               <groupId>javax.servlet</groupId>
               <artifactId>javax.servlet-api</artifactId>
               <version>3.0.1</version>
               <scope>provided</scope>
           </dependency>
    3.web.xml,加入过滤器(因为代理过滤器需要找到真实过滤器)
        <!--shiro需要找到真实过滤器-->
       <!--shiro代理过滤器-->
     <filter>
       <filter-name>shiroFilter</filter-name>
       <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
       <init-param>
         <param-name>targetFilterLifecycle</param-name>
         <param-value>true</param-value>
       </init-param>
     </filter>

     <filter-mapping>
       <filter-name>shiroFilter</filter-name>
       <url-pattern>/*</url-pattern>
     </filter-mapping>
    4.applicationContext-shiro.xml配置文件
    Itsource_auth_shiro中的applicationContext-shiro.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
          xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">

       

       <!--shiro的核心对象-->
       <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
                 <!--配置realm-->
           <property name="realm" ref="authRealm"/>
       </bean>

       <!--Realms-->
       <bean id="authRealm" class="cn.itsource.shiro.realm.AuthenRealm">
           <property name="credentialsMatcher">
               <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                   <property name="hashAlgorithmName" value="MD5"/>
                   <property name="hashIterations" value="10"/>
               </bean>
           </property>
       </bean>

       <!--shiro的过滤器配置 web.xml的代理过滤器名称一样-->
       <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
           <property name="securityManager" ref="securityManager"/>
           <property name="loginUrl" value="/s/login"/>
           <property name="successUrl" value="/s/index"/>
           <property name="unauthorizedUrl" value="/s/unauthorized"/>
         
           <property name="filterChainDefinitions">
               <value>
                  /login = anon
                  /** = authc
               </value>
           </property>
       </bean>


    </beans>
    5.创建一个realm自定义的类,做认证功能:
    public class AuthenRealm extends AuthorizingRealm {

       @Autowired
       private IEmployeeService employeeService;
       //授权方法
       @Override
       protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
           return null;
      }

       //认证
       @Override
       protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
           //得到token的令牌
           UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
           // 取到用户名
           String username = token.getUsername();
           //从数据库查询用户
           Employee employee = employeeService.getByUsername(username);
           if(employee==null){
               throw new UnknownAccountException(username);
          }
           //主体
           Object principal = employee;
           //得到数据库密码
           Object hashedCredentials = employee.getPassword();
           //准备一个颜值
           ByteSource credentialsSalt = ByteSource.Util.bytes(MD5Util.SALT);
           String realmName = getName();
           //把从数据库查询出来的信息和当前信息做比较
           SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,hashedCredentials,credentialsSalt,realmName);
           return info;
      }
    }
    6.把配置文件集成到spring,web.xml中

    增加如下:

    classpath*:applicationContext-shiro.xml

    结果如下:

    <context-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>
        classpath*:applicationContext.xml,
        classpath*:applicationContext-shiro.xml
       </param-value>
     </context-param>
     <!--Spring监听器 ApplicationContext 载入 -->
     <listener>
       <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
     </listener>
    7.创建MD5Util类,给字符串加密加盐

    shiro类里面写一个包util,写一个类:MD5Util

    package cn.itsource.shiro.util;

    import org.apache.shiro.crypto.hash.SimpleHash;

    public class MD5Util {

       public static final String SALT = "itsource";

       /**
        * 加密
        * @param source:需要加密的字符串
        * @return
        */
       public static String encrypt(String source){
           //参数:加密的名字,要加密的字符串,盐值,加密次数
           SimpleHash simpleHash = new SimpleHash("MD5",source,SALT,10);
           return simpleHash.toString();
      }

       public static void main(String[] args) {
           System.out.println(encrypt("1"));
      }

    }

    1.3 登录实现

    1.3.1 前台

    (1).准备登陆页面

    (2)点击登录按钮,发送请求方法

    1.login.vue

    handleSubmit2(ev) {
     var _this = this;
     this.$refs.ruleForm2.validate((valid) => {
       if (valid) {
         //_this.$router.replace('/table');
         this.logining = true;
         //NProgress.start();
         var loginParams = { username: this.ruleForm2.account, password: this.ruleForm2.checkPass };
         this.$http.post("/login",loginParams).then(data => {
           this.logining = false;
           let { message, success, resultObj } = data.data;
           if (!success) {
             this.$message({
               message: message,
               type: 'error'
            });
          } else {

               //登录成功跳转/table的路由地址
             sessionStorage.setItem('user', JSON.stringify(resultObj));
             // this.$router.push({ path: '/table' });
               //修改登录成功后跳转到首页
             this.$router.push({ path: '/echarts' });
          }
        });
      } else {
         console.log('error submit!!');
         return false;
      }
    });
    }

    2.在main.js中解开之前注释掉的登录

    /*
    登录权限判断
    router.beforeEach((to, from, next) => {
    //NProgress.start();
    if (to.path == '/login') {
      sessionStorage.removeItem('user');
    }
    let user = JSON.parse(sessionStorage.getItem('user'));
    if (!user && to.path != '/login') {
      next({ path: '/login' })
    } else {
      next()
    }
    })*/

    1.3.2 继续后台

    1.LoginController实现登录
    @Controller
    @CrossOrigin
    public class LoginController {
       /**
        * 身份认证--登录
        * @param employee
        * @return
        */
       @RequestMapping(value = "/login",method = RequestMethod.POST)
       @ResponseBody
       public AjaxResult login(@RequestBody Employee employee){
          //1.获得主体
           Subject subject = SecurityUtils.getSubject();
           
           //通过主体判断是否认证过
           if (!subject.isAuthenticated()){
               //没有认证过
                   //通过账号密码去获得令牌
               String username = employee.getUsername();
               String password = employee.getPassword();
               UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
                   //通过UsernamePasswordToken认证
               try{
                   //调用该方法,即时调用自定义的realm类
                   subject.login(usernamePasswordToken);
                   //不知道账户异常,账号错误
              }catch (UnknownAccountException e){
                   e.printStackTrace();
                   return new AjaxResult("不知道账户异常,账号错误");
                   //错误凭证异常,密码错误
              }catch (IncorrectCredentialsException e){
                   e.printStackTrace();
                   return new AjaxResult("错误凭证异常,密码错误");
                   //认证异常,其他错误
              }catch (AuthenticationException e){
                   e.printStackTrace();
                   return new AjaxResult("认证异常,其他错误");                
              }
               
          }
           Employee employee1 = (Employee) subject.getPrincipal();
           employee.setPassword(null);

           //除了返回登录成功与否,还要把登录的用户返回前端,放到前台的session
           return AjaxResult.me().setResultObj(employee1);
      }

    }
    2.AjaxResult:添加字段

    因为前台需要success、message字段,还有对象信息,需要AjaxResult增加字段

    AjaxResult类中,添加代码:

        public AjaxResult setResultObj(Object resulObj) {
           this.resultObj = resultObj;
           return this;
      }

    完整内容:

    /**
    * Ajax请求的返回内容:增删改
    *   success:成功与否
    *   message:失败原因
    */
    public class AjaxResult {

       private boolean success = true;
       private String message = "操作成功!";
       private Object resultObj = null;

       public boolean isSuccess() {
           return success;
      }

       //链式编程,可以继续. 设置完成后自己对象返回
       public AjaxResult setSuccess(boolean success) {
           this.success = success;
           return this;
      }

       public String getMessage() {
           return message;
      }

       public AjaxResult setMessage(String message) {
           this.message = message;
           return this;
      }

       //默认成功
       public AjaxResult() {
      }

       //失败调用
       public AjaxResult(String message) {
           this.success = false;
           this.message = message;
      }

       public Object getResultObj() {
           return resultObj;
      }

       public AjaxResult setResultObj(Object resulObj) {
           this.resultObj = resultObj;
           return this;
      }

       //不要让我创建太多对象
       public static AjaxResult me(){
           return new AjaxResult();
      }

       public static void main(String[] args) {
           AjaxResult.me().setMessage("xxx").setSuccess(false);
      }
    }

    4.Service层:

    IEmployeeService

    public interface IEmployeeService extends IBaseService<Employee> {
       /**
        * 添加租户员工
        * @param employee
        */
       void addTenantEmployee(Employee employee);

       Employee getByUsername(String username);
    }

    EmployeeServiceImpl :

    @Service
    public class EmployeeServiceImpl extends BaseServiceImpl<Employee> implements IEmployeeService {

       @Autowired
       private TenantMapper tenantMapper;

       @Autowired
       private EmployeeMapper employeeMapper;
       @Override
       public void addTenantEmployee(Employee employee) {
           Tenant tenant = employee.getTenant();
           tenant.setRegisterTime(new Date());
           tenant.setState(0);
           //添加租户返回租户id   添加前对象里面没有id,添加完成后就有了
           tenantMapper.save(tenant);
           //把租户id设置给员工
           employee.setTenant(tenant);
           //在保存员工
           employee.setRealName(employee.getUsername());
           employeeMapper.save(employee);
      }

       @Override
       public Employee getByUsername(String username) {
           return employeeMapper.loadByUsername(username);
      }
    }

    5.Mapper

    EmployeeMapper :

    /**
    * 通过继承baseMapper拥有的基础crud,还可以扩展自己方法
    */
    public interface EmployeeMapper extends BaseMapper<Employee> {
       Employee loadByUsername(String username);
    }

    EmployeeMapper .xml

    <!--Employee loadByUsername(String username);-->
    <select id="loadByUsername" parameterType="string" resultType="Employee">
    select * from t_employee WHERE  username = #{username}
    </select>

    1.2.4 问题:成功后无法访问

    1.2.4.1原因分析:

    前后端分离项目中,ajax请求没有携带cookie,所以后台无法通过cookie获取到SESSIONID,从而无法获取到session对象。而shiro的认证与授权都是通过session实现的。

    解决办法:登录成功后返回token,并以后每次ajax请求都携带token

    1.2.4.2 后端代码实现

    1)登录成功后返回token,并以后每次ajax请求都要携带token

    1.LoginController中

    //课件中的代码:

     Employee employee1 = (Employee) currentUser.getPrincipal();
           employee.setPassword(null);

           Map<String,Object> result = new HashMap<>();
           result.put("user",employee1);
           System.out.println(currentUser.getSession().getId()+"xxxx"); 登录成功后把会话id返回,会后作为token使用
           result.put("token",currentUser.getSession().getId());

           return AjaxResult.me().setResultObj(result);

    //老师写的代码:

            //把employee信息传到前台,前台放入session(前台session)
           Employee employee1 = (Employee)subject.getPrincipal();
           UserContext.setUser(employee1);
           AjaxResult ajaxResult = new AjaxResult();
           Map mp = new HashMap<>();
           mp.put("user",employee1);
           //jsessionid -->token
           mp.put("token",subject.getSession().getId());
           ajaxResult.setResultObj(mp);
           //返回对象

    1.2.4.3 前端代码实现

    1.Longin.vue中,登录后跳转

    this.$http.post("/login",loginParams).then(data => {
                 this.logining = false;
                 let { success, message, resultObj } = data.data;
                 if (!success) {
                   this.$message({
                     message: message,
                     type: 'error'
                  });
                } else {
                     console.log(resultObj)
                     //登录成功跳转/table的路由地址
                   sessionStorage.setItem('user', JSON.stringify(resultObj.user));
                   sessionStorage.setItem('token', resultObj.token); //不要加字符串转换了巨大的坑
                     //修改登录成功后跳转到首页
                   this.$router.push({ path: '/echarts' });
                }

    2.Home.vue中,退出登录

        //退出登录
    logout: function () {
    var _this = this;
    this.$confirm('确认退出吗?', '提示', {
    //type: 'warning'
    }).then(() => {
    sessionStorage.removeItem('user');
    sessionStorage.removeItem('token');
    _this.$router.push('/login');
    }).catch(() => {

    });

    3.Main.js中,每次请求都拦截,把x-token设置到请求头中

    //拦截器 
    axios.interceptors.request.use(config => {
       if (sessionStorage.getItem('token')) {
           // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
           config.headers['X-Token'] = sessionStorage.getItem('token')
      }
       console.debug('config',config)
       return config
    }, error => {
       // Do something with request error
       Promise.reject(error)
    })
    1.2.4.3 后端代码实现

    服务端变为通过token来唯一标识session

    1.Shiro spring配置文件中,applicationContext-shiro.xml中:

      <!--session管理器-->
       <bean id="sessionManager" class="cn.itsource.shiro.util.CrmSessionManager"/>

       <!--shiro的核心对象-->
       <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
           <property name="sessionManager" ref="sessionManager"/>
           <!--配置realm-->
           <property name="realm" ref="authRealm"/>
       </bean>

    2.创建CrmSessionManager类,并继承DefaultWebSessionManager类

    如果请求到后台没有sessionid,则设置sessionid,有则获取sessionid

    package cn.itsource.shiro.util;

    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.subject.Subject;
    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 javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import java.io.Serializable;

    /**
    *
    * 传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,
    * 在前后端分离的项目中(也可在移动APP项目使用),我们选择在ajax的请求头中传递sessionId,
    * 因此需要重写shiro获取sessionId的方式。
    * 自定义CrmSessionManager类继承DefaultWebSessionManager类,重写getSessionId方法
    *
    */
    public class CrmSessionManager extends DefaultWebSessionManager {

       private static final String AUTHORIZATION = "X-TOKEN";

       private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

       public CrmSessionManager() {
           super();
      }

       @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
       //取到jessionid
           String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
           HttpServletRequest request1 = (HttpServletRequest) request;
           //如果请求头中有 X-TOKEN 则其值为sessionId
           if (!StringUtils.isEmpty(id)) {
               System.out.println(id+"jjjjjjjjj"+request1.getRequestURI()+request1.getMethod());
               request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
               request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
               request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
               return id;
          } else {
               //否则按默认规则从cookie取sessionId
               return super.getSessionId(request, response);
          }
      }
    }

    3.xml中

    跨域预检查放行:设置OPTIONS请求的放行

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
          xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">

    <bean id="myAuthc" class="cn.itsource.shiro.util.MyAuthenticationFilter"/>

       <!--shiro的过滤器配置-->
       <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
           <property name="securityManager" ref="securityManager"/>
           <property name="loginUrl" value="/s/login"/>
           <property name="successUrl" value="/s/index"/>
           <property name="unauthorizedUrl" value="/s/unauthorized"/>
           <property name="filters">
               <map>
                   <entry key="myAuthc" value-ref="myAuthc"/>
               </map>
           </property>
           <property name="filterChainDefinitions">
               <value>
                  /login = anon
                  /** = myAuthc
               </value>
           </property>
       </bean>

    创建MyAuthenticationFilter类,继承FormAuthenticationFilter类,覆写isAccessAllowed方法,重新设置放行方法

    package cn.itsource.shiro.util;

    import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;

    /**
    * 自定义身份认证过滤器
    */
    public class MyAuthenticationFilter extends FormAuthenticationFilter {

      @Override
      protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
          //如果是OPTIONS请求,直接放行
          HttpServletRequest httpServletRequest = (HttpServletRequest) request;
          String method = httpServletRequest.getMethod();
          System.out.println(method);
          if("OPTIONS".equalsIgnoreCase(method)){
              return true;
          }
          return super.isAccessAllowed(request, response, mappedValue);
      }
    }

    1.4 抽取登录用户的代码

    1.创建UserContext类,专门用来登录调用

    package cn.itsource.shiro.util;

    import cn.itsource.domain.Employee;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.subject.Subject;

    /**
    * 当前登录用户相关
    */
    public class UserContext {
       private static final String CURRENT_LOGIN_USER=  "loginUser";

       /**
        * 设置当前登录用户
        * @param employee
        */
       public static void setUser(Employee employee){
           Subject currentUser = SecurityUtils.getSubject();
           currentUser.getSession().setAttribute(CURRENT_LOGIN_USER,employee);
      }

       /**
        * 获取当前登录用户
        * @return employee
        */
       public static Employee getUser(){
           Subject currentUser = SecurityUtils.getSubject();
           return (Employee) currentUser.getSession().getAttribute(CURRENT_LOGIN_USER);
      }
    }

    2.现在登录的代码:

    //以下获取当前登录用户存在问题如下:
    //1 到处都散落获取当前登录用户代码
    //2 以后不用shiro所有的地方都要改变
    //解决方案:封装一个方法获取当前登录用户,以后变了只需要修改这个方法就ok了
    Subject currentUser = SecurityUtils.getSubject();
    Object loginUser = currentUser.getSession().getAttribute("loginUser");



  • 相关阅读:
    二 Capacity Scheduler 计算能力调度器
    一:yarn 介绍
    2.hbase原理(未完待续)
    1.安装hbase
    创建第一个vue项目
    初学vue(二)
    第一次面试
    面试题
    C#冒泡排序
    面试真题(.NET/Sqlserver/web前端)
  • 原文地址:https://www.cnblogs.com/htq29study/p/12080185.html
Copyright © 2011-2022 走看看