shiro

1 介绍 :背景.....发展.....此处省略废话一万句

有啥用?: 鉴权框架

 为啥用他?:相比Spring家族的鉴权系统,shiro是轻量级的鉴权框架,可做基于session的有状态登录,基于token无状态的登录的,底层是一串的过滤器链呗

注意点: shiro底层还是依赖Servlet,不能和SpringCloudGateWay集成

2 缓存问题:

shiro本身不实现cache,但是对cache做了抽象,使用缓存需要自身实现

3 常用场景:

单体架构:MD5,redis,依赖sessionn状态
微服务: 单点登录,jwt,redis,md5,关闭session,没次登录都做拦截,检验token信息

4 依赖注解对接口做鉴权:

/**
 *  @RequiresAuthentication
 *   表示当前Subject已经通过login 进行了身份验证
 * <p>
 *   @RequiresUser
 *   表示当前Subject已经身份验证或者通过记住我登录的。
 * <p>
 *   @RequiresGuest
 *   表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
 * <p>
 *   @RequiresRoles 角色认证
 * <p>
 *   @RequiresPermissions 验证权限的注解
 */

5 demo: 

shiro导包问题
配置ShiroFilterFactorBean
导入
     <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.4.2</version>
        </dependency>
配置 ShiroFilterChainDefinition
  <dependency>
             <groupId>org.apache.shiro</groupId>
             <artifactId>shiro-spring-boot-web-starter</artifactId>
             <version>1.4.0-RC2</version>
         </dependency>

 本次只做了基于token配合Redis的无状态登录案例:

(1) 签发token

package com.example.newshiro202304;

import com.example.shiro.utils.JwtUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
public class Controller {



    /**
     * 主要是做密码和用户的验证,然后签发token
     * (1) 验证密码
     * (2) 检验redis中当前账号的存储个数,限制登录人数
     * (3) 将token和ip绑定,防止token被挟持
     * (4) 签发token
     * @param password
     * @param account
     * @param request
     * @return
     */
    @RequestMapping("/login")
    public Object login(String password, String account, HttpServletRequest request) {
       User user  = mapper.select(account)  ;
        //检验密码,将密码加密
       if(!uset.getPassword.equals(Md5.jiami(password))){
           throw new Exception("密码不对");
       }
        //在redis中获取当前已经登录的人员的数量,根据账号生成和的token判断
       int num=redisUtils.getprefix(account+"token").size();
       if (num>3){
           throw new Exception("登录人数限制为3");
       }
       //生成token
        final String token = JwtUtils.getToken(account, account);
        //将token存在redis,同时将对应的IP也存在redis中,设置过期时间为30min
        redisUtils.set(token+"ip",request.getRemoteHost(),30);
        redisUtils.set(token,token,30);
        //签发token
        return token;
    }
}

(2) token:

package com.example.newshiro202304;

import org.apache.shiro.authc.AuthenticationToken;

public class JwtToken implements AuthenticationToken {
    private String token;

    public JwtToken() {

    }

    public JwtToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }


}

(3)redis配置:

package com.example.newshiro202304;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;//LettuceConnectionFactory 其实就是这个

    @Bean
    public RedisTemplate<String,Object> redisTemplate(){
        final RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new JdkSerializationRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }


}

(4) shiro 的基本配置:

package com.example.newshiro202304;

import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.cache.RedisCacheManager;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.logging.Logger;

@Configuration
public class ShiroConfig {
    //private static final Logger log= LoggerFactory.getLogger(ShiroConfig.class);

    @Autowired
    private Environment env;

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        final ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        final HashMap<String, Filter> map = new HashMap(1);
        //map.put("jwt",new JwtFilter());
        shiroFilterFactoryBean.setFilters(map);
        final LinkedHashMap<String, String> lMap = new LinkedHashMap<>();
        lMap.put("/login", "anno");
        lMap.put("/get", "user");
        //lMap.put("/**", "atuhc");//一般采用自定义的,单个服务为不涉token共享时可用
        lMap.put("/**", "jwt");//集群环境下需要自己设置过滤规则
        //lMap.put("/**", "jwt[manger,user]");//集群环境下需要自己编写过滤器,参数对应过去其中的Object mappedValue字符串数组,检测实体类中的第一页个集合(权限或者角色)
        shiroFilterFactoryBean.setLoginUrl("/login");//设置登录接口
        shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");//未授权错误页面
        shiroFilterFactoryBean.setFilterChainDefinitionMap(lMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager securityManager(ShiroRelm relm) {
        final DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(relm);

        //关闭自带的session,强制每次登录都做检测
        final DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
        final DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        defaultSubjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        defaultWebSecurityManager.setSubjectDAO(defaultSubjectDAO);

        //自定义缓存实现
        //defaultWebSecurityManager.setCacheManager(new RedisCacheManager());

        //自定义sessio管理
        //defaultWebSecurityManager.setSessionManager(new SessionManager());

        //引入两种身份验证
        // SecurityUtils.setSecurityManager(defaultWebSecurityManager);

        //多个Relam认证策略
        //defaultWebSecurityManager.setRealms();
        //多Relm的设置认证策略设置
        //defaultWebSecurityManager.setAuthenticator();
        return defaultWebSecurityManager;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        final AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * 解决引入AOP后controller中加入角色检验注解导致接口映射失效
     *
     * @return
     */
    //@Bean
    // @DependsOn("lifeBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        final DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }

    // @Bean
   //public RedisCacheManager redisCacheManager() {
   //    final RedisCacheManager redisCacheManager = new RedisCacheManager();
   //    //这个RedisCacheManager导报有问题
   //    //redisCacheManager.setRedisManger(redismanger)
   //    //shiro-redis需要一个id字段来标识在redis中的授权对象,其实就是为了缓存对象,new SimpleAuthenticationInfo(loginuser,token,getName())
   //    // redisCacheManager.setPrincipalIdFieldName("id");
   //    //设置缓存失效时间
   //    // redisCacheManager.setExpire(300000);

   //    return redisCacheManager;
   //}

   //@Bean
   //public RedisManger redisManger(){
   //    RedisManger redisManger=new RedisManger();
   //    //todo,将redis的链接信息设置
   //    return redisManger;
   //}


}

(5) realm,鉴权流程就是经过这个做的映射:

package com.example.newshiro202304;

import com.example.shiro.utils.JwtUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;

import org.apache.shiro.subject.PrincipalCollection;

import java.util.HashSet;
import java.util.List;

public class ShiroRealm extends AuthorizingRealm {
    public ShiroRealm() {

    }

    /**
     * 自定义的token需要重写
     *
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 鉴权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        final SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        if (principalCollection == null) {
            return info;
        }
        final User user = (User) principalCollection.getPrimaryPrincipal();
        List<String> roles = mapper.selectRoles(user.getID);
        List<String> licendes = mapper.selectLicendes(user.getID);
        //将角色和权限信息刷到对象中
        info.setRoles(new HashSet<>(roles));
        info.setObjectPermissions(new HashSet(licendes));
        return info;
    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        if (token == null) {
            throw new AuthenticationException();
        }
        User user = checkToken(token);

        final SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, token, getName());

        return simpleAuthenticationInfo;

    }

    private User checkToken(String token) {
        final String account = JwtUtils.getAccount(token);
        if (account == null) {
            throw new AuthenticationException();
        }
        //检测token
        String rtoken = redisUtils.get(token);
        String ip = redisUtils.get(token + "ip");
        if (rtoken == null | !request.getIP.equals(ip)) {
            //token失效或者被挟持
            throw new AuthenticationException();
        }
        //续约token
        JwtTokenRefresh(token, rtoken, account,ip);
        //从数据库或者reis缓存中查询redis,这一步步自己实现
        return user;

    }

    private void JwtTokenRefresh(String token, String rtoken, String account,String ip) {
        //jwt生成的token有效是十五分钟,但是redis存储是半个小时,这样每次检查都是redis 中的token
        if (JwtUtils.check(rtoken, account, account)) {
            //redis中的token还能用,
            return;
        }
        //重新生成token,续约
        final String newtoken = JwtUtils.getToken(account, account);
        redisUtils.set(token, newtoken,30);
        redisUtils.set(token+"ip",ip,30);
    }
}

(6) 自定义过滤器:

package com.example.newshiro202304;

import com.alibaba.fastjson.JSONObject;
import lombok.SneakyThrows;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;

/***
 * 这个类不能添加Spring的注解,否则无法加载
 */
public class JwtFilter extends BasicHttpAuthenticationFilter {

    @SneakyThrows
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            //token认证
            executeLogin(request, response);
            //鉴权
            return checkRoleOrPrim(request, response, mappedValue);
        } catch (Exception e) {
            //登录失败,鉴权失败
            final HttpServletResponse response1 = (HttpServletResponse) response;
            response1.setHeader("Context-type", "application/json;charset=UTF-8");
            final HashMap<String, String> data = new HashMap<>();
            data.put("data", "");
            data.put("code", "403");
            data.put("msg", "无权限");
            final String s = JSONObject.toJSONString(data);
            response1.getWriter().write(s);
            return false;

        }
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest request1 = (HttpServletRequest) request;
        final String header = request1.getHeader("X-Access-Token");
        final JwtToken jwtToken = new JwtToken(header);
        Subject subject = SecurityUtils.getSubject();
        subject.login(jwtToken);
        return true;
    }

    private boolean checkRoleOrPrim(ServletRequest request, ServletResponse response, Object mappedValue) {
        final Subject subject = getSubject(request, response);
        //可以转换为用户对象
        //final Object principal = subject.getPrincipal();
        //获取的是对象中的第一个集合
        final PrincipalCollection principals = subject.getPrincipals();
        //(String[])mappedValue为用户权限
        return subject.isPermittedAll((String[]) mappedValue);
    }

    /**
     * token超时
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //saveRequest(request);//保存请求,重新登录后跳转到
        //redirectToLogin(request,response);重定向到登录页面

        return false;

    }

    //处理跨域
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest request1 = (HttpServletRequest) request;
        HttpServletResponse response1 = (HttpServletResponse) response;
        response1.setHeader("Access-Control-Allow-Origin", request1.getHeader("Origin"));
        response1.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        response1.setHeader("Access-Control-Allow-Headers", request1.getHeader("Access-Control-Request-Headers"));

        if (request1.getMethod().equals("OPTIONS")) {
            response1.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

注意对跨域做支持:  options跨域请求不会先发送options,没有带cookie信心,在过滤

(7) 鉴权接口:

package com.example.shiro.controller;

import com.example.shiro.entity.SysUser;
import com.example.shiro.mapper.UserMapper;
import com.example.shiro.realm.old.Redis;
import com.example.shiro.utils.JwtUtils;
import com.example.shiro.utils.RedisUtils;
import com.example.shiro.utils.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.repository.query.Param;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.ServletResponse;
import java.util.Map;

/**
 *  @RequiresAuthentication
 *   表示当前Subject已经通过login 进行了身份验证
 * <p>
 *   @RequiresUser
 *   表示当前Subject已经身份验证或者通过记住我登录的。
 * <p>
 *   @RequiresGuest
 *   表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
 * <p>
 *   @RequiresRoles 角色认证
 * <p>
 *   @RequiresPermissions 验证权限的注解
 */

@Slf4j
@RestController
public class ShiroController {

    @Autowired
    private UserMapper mapper;

    @Autowired
    private RedisUtils redisUtils;


    //@GetMapping("login")
    public Object login(String account, String password) {
        log.info("login account is :{}", account);
        log.info("login password is :{}", password);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(account, password);
        subject.login(token);
        return "login";
    }

    @PostMapping("login")
    public void JwtLogin(@RequestBody Map map) {
        log.info("login account and pass is :{}", map);
        final String account = (String) map.get("account");
        final String password = (String) map.get("password");
        final SysUser userInfo = mapper.getUserInfo(Long.valueOf(account));
        if (userInfo == null || !userInfo.getPassword().equals(password)) {
            throw new AccountException("account is mess");
        }
        final String token = JwtUtils.getToken(account, password);
        SpringUtil.getResponse().setHeader("X-Access_Token", token);
        redisUtils.set("UserToken_"+account+token,token,1800L);
        redisUtils.set("UserUrl_"+account+token,SpringUtil.getHostMsg(),1800L);
    }


    @RequiresRoles("manger")
    @GetMapping("role")
    public Object role() {
        return "role";
    }

    @RequiresPermissions("insert")
    @GetMapping("permissions")
    public Object permissions() {
        return "permissions";
    }

    @RequiresRoles("manger")
    @RequiresPermissions("insert")
    @GetMapping("all")
    public Object roleAndPermissions() {
        return "roleAndPermissions";
    }

    @RequiresPermissions("delect")
    @GetMapping("err")
    public Object err_test() {
        return "eer===";
    }


    @Resource(name = "redisTemplate", type = RedisTemplate.class)
    private RedisTemplate template;

    @GetMapping("redis")
    public void getMsg() {
        template.opsForValue().set("123456", 123456);
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值