Springboot集成Shiro,实现权限和角色管理

项目需求:

     1. 在SpringBoot项目基础上实现登录功能

     2.使用传统的Token, 登录成功使用UUID生成对应的Token存到Redis中,并设置过期时间,使用Shiro来完成登录,并对用户实现校验和权限管理

Shiro使用的问题:

     Shiro本身使用的传统的Cookie和Session来管理用户信息,因为需要用到Token,所以需要把Shiro本身的Token转化为我们所自定义的Token,

每次登陆以后通过在请求头中增加我们使用的Token来判断是否登录以及是否有权限。

首先引入了Shiro官方提供的SpringBoot包,整合了注解,配置等一些功能,这个包需要在Maven仓库官网搜索才可以导入          (Shiro官网的下载不了-_-) 这里贴下官网https://shiro.apache.org/spring-boot.html

<dependency>

    <groupId>org.apache.shiro</groupId>

    <artifactId>shiro-spring-boot-starter</artifactId>

    <version>1.4.1</version>

</dependency>

Shiro主要的实现方法是自定义一个自己的Realm通过这里进行身份认证与鉴权

@Slf4j

@Component("myRealm")

public class MyRealm extends AuthorizingRealm {

    @Autowired

    private ManagerService managerService;

    /**

     * 提供用户信息返回权限信息

     */

    @Override

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        log.info("授权");

        Manager manager = (Manager) principals.getPrimaryPrincipal();

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        info.addRole("2");

        //在这里可以通过去表里查到用户的角色和权限,加到Shiro中

        //TODO:增加角色和权限

        return info;

    }

    /**

     * 使用自定义Token替代原生Token

     *这里需要自定义一个Token类来表示我们所使用的Token而不是Shiro官方的Token

     * @param token

     * @return

     */

    @Override

    public boolean supports(AuthenticationToken token) {

        return token instanceof Token;

    }

    /**

     * 提供账户信息返回认证信息

     */

    @Override

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //这里的Token就是我们定义的Token,我们需要通过在拦截器去获取我们需要的Token在这里进行登录校验

        String myToken = (String) token.getCredentials();

        log.info("登录认证" + myToken);

        try {

            if (myToken == null) {

                throw new AuthenticationException("Token didn't existed!");

            }

            Manager manager = managerService.findByToken(myToken);

            if (manager != null) {

                //TODO:可以判断是否禁用

                //这里返回manager对象是为了在Controller中可以通过   

                //Manager manager = (Manager)SecurityUtils.getSubject().getPrincipal();来获取我们需要的用户,减少一次查询也不                  //需要在请求中传入用户信息

                return new SimpleAuthenticationInfo(manager, myToken, getName());

            } else {

                通过丢出异常让异常处理器接受到从而让接口无法让其他人访问

                throw new AuthenticationException("Token didn't existed!");

            }

        } catch (AuthenticationException e) {

            throw new AuthenticationException("Exception!");

        }

    }

}

接下里是我们自己实现的Token类

public class Token implements AuthenticationToken {   //需要继承Shiro的这个类来表示需要代替的Token


    /**

     * Token

     */

    private String token;

    public Token(String token) {

        this.token = token;

    }

    @Override

    public Object getPrincipal() {

        return token;

    }

    @Override

    public Object getCredentials() {

        return token;

    }

}

接下来就是关键的一步通过在过滤器中找到我们的请求头让我们的Token去Realm中验证

@Slf4j

public class TokenFilter extends BasicHttpAuthenticationFilter {

    /**

     *登录标识  请求头中带上此标志来进行验证

     */

    private static String LOGIN_SIGN = "Token";

    /**

     * 检测用户是否登录

     * 检测header里面是否包含Token字段即可

     *

     * @param request

     * @param response

     * @return

     */

    @Override

    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {

        HttpServletRequest req = (HttpServletRequest) request;

        String authentication = req.getHeader(LOGIN_SIGN);

        log.info(authentication);

        if (StringUtil.isEmpty(authentication)) {

            log.error("没有token");

        }

        return authentication != null;

    }

    @Override

    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {

        log.info("判断请求的请求头是否带上 Token");

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        //这里让所有的请求除了登录注册都去找Realm验证没有游客功能,过滤登录注册也可以在Config里面配置不过还没有验证

        if (httpServletRequest.getRequestURI().equals("/login") || httpServletRequest.getRequestURI().equals("/register")) {

            return true;

        }

        if ((httpServletRequest).getHeader(LOGIN_SIGN) != null) {

            //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确

            try {

                executeLogin(request, response);

                return true;

            } catch (Exception e) {

                //token 错误

                response401(request, response, e.getMessage());

            }

        }

        //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true

        return false;

    }

    //如果上面的方法返回false也就是没有请求头带token就需要返回到登录界面,因为是前后端分离项目,所以需要通过Json返回给前端,前端再进行判断

    @Override

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {

        PrintWriter out = null;

        HttpServletResponse res = (HttpServletResponse) response;

        res.setStatus(HttpStatus.UNAUTHORIZED.value());

        res.setCharacterEncoding("UTF-8");

        res.setContentType("application/json; charset=utf-8");

        try {

            out = res.getWriter();

            ResultDTO result = new ResultDTO(false, 401, "请先登录");

            out.append(JsonUtil.marshal(result));

        } catch (IOException e) {

            log.error("返回Response信息出现IOException异常:" + e.getMessage());

        } finally {

            if (out != null) {

                out.close();

            }

        }

        return false;

    }


    //如果成功来这里进行登录 这里的Token就是我们所设定的Token,将Token获取再去Realm里面进行验证

    @Override

    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {

        HttpServletRequest req = (HttpServletRequest) request;

        String header = req.getHeader(LOGIN_SIGN);

        Token token = new Token(header);

        // 提交给realm进行登入,如果错误他会抛出异常并被捕获

        getSubject(request, response).login(token);

        // 如果没有抛出异常则代表登入成功,返回true

        return true;

    }

    /**

     * 对跨域提供支持

     *

     * @param request

     * @param response

     * @return

     * @throws Exception

     */

    @Override

    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));

        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");

        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));

        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态

        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {

            httpServletResponse.setStatus(HttpStatus.OK.value());

            return false;

        }

        return super.preHandle(request, response);

    }

    /**

     * 401非法请求

     */

    private void response401(ServletRequest req, ServletResponse resp,String msg) {

        HttpServletResponse httpServletResponse = (HttpServletResponse) resp;

        httpServletResponse.setStatus(HttpStatus.OK.value());

        httpServletResponse.setCharacterEncoding("UTF-8");

        httpServletResponse.setContentType("application/json; charset=utf-8");

        PrintWriter out = null;

        try {

            out = httpServletResponse.getWriter();

            ResultDTO result = new ResultDTO(false, 401, msg);

            out.append(JsonUtil.marshal(result));

        } catch (IOException e) {

            log.error("返回Response信息出现IOException异常:" + e.getMessage());

        } finally {

            if (out != null) {

                out.close();

            }

        }

    }

}

拦截器配置好就可以去配置Shiro的配置

@Configuration                             //来表示是一个配置类

public class ShiroConfig {

    //前几个配置不知道啥意思,好像没啥用(-_-)

    @Bean

    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {

        return new LifecycleBeanPostProcessor();

    }

    @Bean

    @DependsOn("lifecycleBeanPostProcessor")

    public static DefaultAdvisorAutoProxyCreator getLifecycleBeanPostProcessor() {

        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();

        // 强制使用cglib

        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);

        defaultAdvisorAutoProxyCreator.setUsePrefix(true);

        return defaultAdvisorAutoProxyCreator;

    }

    @Bean

    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {

        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();

        advisor.setSecurityManager(securityManager());

        return advisor;

    }

    @Bean

    public DefaultWebSubjectFactory subjectFactory() {

        return new SubjectFactory();

    }

    //将我们写好的的realm注入

    @Bean

    public Realm realm() {

        return new MyRealm();

    }

    //在这里配置我们的过滤器

    @Bean

    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //注意过滤器配置顺序 不能颠倒

        Map<String, Filter> filterMap = new HashMap<>();

        filterMap.put("token", tokenFilter());      //这里加入我们的过滤器

        shiroFilterFactoryBean.setFilters(filterMap); 

        filterChainDefinitionMap.put("/**", "token");  //这里的名字需要和上面添加的相同

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;

    }

    //需要把Shiro的cookie和session关闭但是好像网上找不到正确的方法,每次请求都会带一个sessionId的cookie这以后还要多分析下

    @Bean

    public DefaultWebSecurityManager  securityManager(){

        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();

        securityManager.setRealm(realm());

        //关闭shiro自带的session

        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();

        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();

        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);

        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);

        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;

    }

    @Bean

    protected SessionStorageEvaluator sessionStorageEvaluator() {

        DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();

        sessionStorageEvaluator.setSessionStorageEnabled(false);

        return sessionStorageEvaluator;

    }

    //我们所自定义的过滤器

    @Bean

    public TokenFilter tokenFilter() {

        return new TokenFilter();

    }

}

Shiro默认的没有登录是跳转login.jsp因为是前后端分离的项目所以需要修改为返回Json,但是我没有用到这个先写一下

public class MyAuthenticationFilter extends FormAuthenticationFilter {

    @Override

    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        return super.isAccessAllowed(request, response, mappedValue);

    }

    @Override

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        Subject subject = SecurityUtils.getSubject();

        Object user = subject.getPrincipal();

        if (((HttpServletRequest) request).getHeader("Authentication") == null) {

            ResultDTO resultDTO = new ResultDTO(false, 401, "未登录");

            httpServletResponse.setCharacterEncoding("UTF-8");

            httpServletResponse.setContentType("application/json");

            httpServletResponse.getWriter().write(JSONObject.toJSONString(resultDTO, SerializerFeature.WriteMapNullValue));

        }

        return false;

    }

}

接下来就是登录了

@PostMapping("/login")

public ResultDTO managerLogin(@RequestBody Map<String, Object> args, HttpServletRequest request, HttpServletResponse response) {

                //这里不要去用Shiro的登录来登录,需要我们自己去数据库查询,所以就不写了

                String token = UUID.randomUUID().toString();

                //存入Redis并设置过期时间

                redisTemplate.opsForValue().set(String.format(RedisConstant.TOKEN_PREFIX, token), username, expire, TimeUnit.SECONDS);

                //在响应头添加Token让前端来获取

                response.setHeader("Token", token);


}

通过注解来进行角色权限判断

@RequiresGuest

只有游客可以访问

@RequiresAuthentication

需要登录才能访问

@RequiresUser

已登录的用户或“记住我”的用户能访问

@RequiresRoles

已登录的用户需具有指定的角色才能访问

@RequiresPermissions

已登录的用户需具有指定的权限才能访问

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335