安全框架--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");