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

    org.apache.shiro

    shiro-spring-boot-starter

    1.4.1

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 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 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

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

你可能感兴趣的:(Springboot集成Shiro,实现权限和角色管理)