shiro入门

一,背景

最近工作需要,研究了下shiro,记录下过程,便于以后回顾,shiro介绍不多说,网上一堆

二,代码

2.1 引包

  


            org.apache.shiro
            shiro-spring
            1.12.0
        



        
            org.springframework.boot
            spring-boot-starter-data-redis-reactive
        

        
            org.crazycake
            shiro-redis
            3.3.1
        

2.1 配置类

import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroFilterConfiguration {

    private final String CACHE_KEY="shiro:cache:";

    private final String SESSION_KEY = "shiro:session:";

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.timeout}")
    private int timeout;


    /**
     * 开启注解
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 这个开启shir过滤
     * 这个方法主要设置过滤规则
     * 
     */
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilter(DefaultWebSecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 自定义拦截器,这里可自己自定义过滤器,当然还可以用自带的几种
        Map customFilterMap = new LinkedHashMap<>();
        customFilterMap.put("shiroAuthenticationFilter", new ShiroAuthenticationFilter());
        shiroFilterFactoryBean.setFilters(customFilterMap);
        //shiro登录接口需要跳过过滤器,anon的意思不需要拦截,还有其他好几种
        Map map = new HashMap<>();
        map.put("/login","anon");
        //设置其他接口需要拦截的,使用自定义的过滤器
        map.put("/**", "shiroAuthenticationFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

     /**
     * 把自定义的一些东西set到DefaultWebSecurityManage中
     * 自定义菜单权限,session管理器,缓存管理器等还有其他一些
     *  基本很多默认的东西都可以通过继承来重写
     */
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(getMyRealm());
        securityManager.setSessionManager(sessionManager());
        securityManager.setCacheManager(redisCacheManager());
        return securityManager;
    }

    /**
     * MyRealm 中定义了自己去数据库查的菜单和权限放入shiro中
     * 
     * 需要继承AuthorizingRealm
     */
    @Bean
    public MyRealm getMyRealm() {
        MyRealm myRealm = new MyRealm();

        return myRealm;
    }

    /**
     * 自定义cookie名称
     * sessionId的key可以自定义,默认把sessionid放在cookie中
     * 也可以把sessionId放在请求头中,只要登录时返回sessionId就行,然后让前端把sessionId放在头中,都可以自定义。
     */
    @Bean
    public SimpleCookie sessionIdCookie(){
        SimpleCookie cookie = new SimpleCookie("X-Token");
        cookie.setMaxAge(-1);
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        return cookie;
    }

     /**
     * 自定义SessionManager管理器
     * 通过set一些自定义属性
     * 
     */
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //自定义cookie 中sessionId 的key
        sessionManager.setSessionIdCookie(sessionIdCookie());
        sessionManager.setSessionIdCookieEnabled(true);
        //删除过期session
        sessionManager.setDeleteInvalidSessions(true);

        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setCacheManager(redisCacheManager());
        // 设置全局session过期时间
        sessionManager.setGlobalSessionTimeout(timeout*1000);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        // 取消URL后面的JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

    /**
     * RedisSessionDAO是操作redis的dao层
     * 
     * 
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
        redisSessionDAO.setKeyPrefix(SESSION_KEY);
        redisSessionDAO.setExpire(timeout);
        return redisSessionDAO;
    }

    /**
     * 设置redis的链接配置,地址和账号密码
     * 
     * 
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host+":" + port);
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        return redisManager;
    }

     /**
     * 设置生成sessionId的规则
     * 
     * ShiroSessionIdGenerator可自定义规则
     */
    @Bean
    public ShiroSessionIdGenerator sessionIdGenerator() {
        return new ShiroSessionIdGenerator();
    }

   /**
     * 设置缓存管理器,这里缓存也用redis
     * 
     * 
     */
    @Bean
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix(CACHE_KEY);
        // 配置缓存的话要求放在session里面的实体类必须有个id标识
        redisCacheManager.setPrincipalIdFieldName("ouid");
        redisCacheManager.setExpire(timeout);
        return redisCacheManager;
    }


2.2 自定义过滤器

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

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

    public ShiroAuthenticationFilter() {
        super();
    }

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (((HttpServletRequest) request).getMethod().equals(REQUEST_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("Content-type", "text/html;charset=UTF-8");
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setContentType("application/json");
        res.setStatus(HttpServletResponse.SC_OK);
        res.setCharacterEncoding("UTF-8");
        PrintWriter writer = res.getWriter();
        Map map = new HashMap<>();
        map.put("code", 500);
        map.put("msg", "未登录");
        writer.write(JSON.toJSONString(map));
        writer.close();
        return false;
    }

2.3 自定义sessionId生成器

public class ShiroSessionIdGenerator implements SessionIdGenerator {

    private static final String REDIS_PREFIX_LOGIN = "login_token_%s";
    @Override
    public Serializable generateId(Session session) {
        Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
        return String.format(REDIS_PREFIX_LOGIN, sessionId);
    }
}

2.4 业务属性权限

public class MyRealm extends AuthorizingRealm {

    //操作数据库,从数据库拿权限
    @Autowired
    private UserRoleSerivce userRoleSerivce;

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        UserRoleModel userRoleModel = userRoleSerivce.queryUserRoleAuthority(Long.parseLong(username));
        AuthenticationInfo info = new SimpleAuthenticationInfo(
                userRoleModel,
                userRoleModel.getPassword(),
                this.getName());
        return info;
    }

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("======授权 =====");

        UserRoleModel userRoleModel = (UserRoleModel) principalCollection.getPrimaryPrincipal();
        List roles = new ArrayList();
        roles.add(userRoleModel.getRoleName());
        List auths = userRoleModel.getAuthorities().stream().map(Authority::getAuthorityName).collect(Collectors.toList());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(roles);
        info.addStringPermissions(auths);
        return info;
    }
}

这个类很重要,菜单角色权限都在这个类中查询数据库set到shiro中

2.5 sessionId不放在cookie中

public class ShiroSessionManager extends DefaultWebSessionManager {
    //定义常量
    private static final String AUTHORIZATION = "Authorization";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
    //重写构造器
    public ShiroSessionManager() {
        super();
        this.setDeleteInvalidSessions(true);
    }
    /**
     * 重写方法实现从请求头获取Token便于接口统一
     * 每次请求进来,Shiro会去从请求头找Authorization这个key对应的Value(Token)
     * @Author Sans
     * @CreateTime 2019/6/13 8:47
     */
    @Override
    public Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果请求头中存在token 则从请求头中获取token
        if (!StringUtils.isEmpty(token)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        } else {
            // 这里禁用掉Cookie获取方式
            // 按默认规则从Cookie取Token
            // return super.getSessionId(request, response);
            return null;
        }
    }
}

如果想sessionid不放在cookie中,放在请求头的话,可以使用这个类。

2.6 全局异常

@ControllerAdvice
public class MyShiroException {
    /**
     * 处理Shiro权限拦截异常
     * 如果返回JSON数据格式请加上 @ResponseBody注解
     * @Author Sans
     * @CreateTime 2019/6/15 13:35
     * @Return Map 返回结果集
     */
    @ResponseBody
    @ExceptionHandler(value = AuthorizationException.class)
    public Map defaultErrorHandler(){
        Map map = new HashMap<>();
        map.put("403","权限不足");
        return map;
    }
}

只要是shiro抛出的异常都统一处理。

2.7 controller

@RestController
public class LoginController {


    @PostMapping("/login")
    public String login(@RequestBody UserRoleModel userRoleModel) {
        System.out.println("username=" + userRoleModel.getOuid() + ",password =" + userRoleModel.getPassword());
        Subject subject  = SecurityUtils.getSubject();

        UsernamePasswordToken token = new UsernamePasswordToken(String.valueOf(userRoleModel.getOuid()),userRoleModel.getPassword(),true);

        try {
            subject.login(token);
            return "成功:" + subject.getSession().getId() ;
        }catch (UnknownAccountException e){
            System.out.println("认证结果: 用户名不正确");
            return "认证结果: 用户名不正确";
        }catch (IncorrectCredentialsException e){
            System.out.println("认证结果:密码不正确 ");
            return "认证结果:密码不正确 ";
        }
    }

    @RequestMapping("/getLogout")
    @RequiresUser
    public Map getLogout(){
        //登出Shiro会帮我们清理掉Session和Cache
        SecurityUtils.getSubject().logout();
        Map map = new HashMap<>();
        map.put("code",200);
        map.put("msg","登出");
        return map;
    }


    @GetMapping("/index")
    public String index() {
        return "index";
    }

    @GetMapping("/auth")
    public String auth() {
        return "已成功登录";
    }

    @GetMapping("/role")
    @RequiresRoles("admin")
    public String role() {
        return "测试admin角色";
    }

    @GetMapping("/permission")
    @RequiresPermissions(value = {"0-select", "5-select"}, logical = Logical.AND)
    public String permission() {
        return "测试Add和Update权限";
    }
}

三 数据库

数据库主要有5张表:用户表,角色表,权限表,用户角色关联表,角色权限关联表。

不做过多叙述,表要哪些字段可根据自己需要

四 参考

shiro还有其他的很多东西可配置,很灵活,还有jsp页面,只不过我这不需要,就没写,其中service,dto操作数据库的代码我就没贴,没必要。

链接:

SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录 - 掘金

Shiro学习文档_shiro中文文档-CSDN博客

SpringBoot集成Shiro极简教程(实战版)_51CTO博客_springboot集成shiro框架

你可能感兴趣的:(java基础,java,开发语言)