springboot shiro jwt实现认证和授权

1.shiro,jwt,oauth2.0是什么?

Shiro:权限框架,可在C/S下运行。 shiro是一套权限管理框架,包括认证、授权等,在使用时直接写相应的接口(小而简单的Shiro就足够)

jwt:是一个鉴权生成加密token的一个名称。

oauth2.0 :一种权限实现标准,是一种安全的授权框架,提供了一套详细的授权机制。用户或应用可以通过公开的或私有的设置,使用第三方认证和授权。

Shiro 有三大核心组件,即 Subject、SecurityManager 和 Realm。

2.依赖引入


            javax.servlet
            javax.servlet-api
            4.0.1
        

        
            org.apache.shiro
            shiro-spring-boot-starter
            1.8.0
        

        
            com.auth0
            java-jwt
            3.18.3
        

3.Jwt的自定义工具类

主要功能如下:

  • 生成符合Jwt机制的token字符串
  • 可以对token字符串进行校验
  • 获取token中的用户信息
  • 判断token是否过期
public class JwtUtil {

    public static final String key = "thisiskey";
    //指定一个token过期时间(毫秒)
    private static final long EXPIRE_TIME = 1000 * 60 * 10;

    /**
     * 生成token
     */
    public static String createJwtToken(String username, List roleList, List accessList) {
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(key);    //使用密钥进行哈希
        // 附带username信息的token
        return JWT.create()
                .withClaim("username", username)
                .withClaim("roles", roleList)
                .withClaim("access", accessList)
                .withExpiresAt(date)  //过期时间
                .sign(algorithm);     //签名算法
    }

    /**
     * 校验token是否正确
     */
    public static boolean verifyToken(String token) {
        try {
            //根据密钥生成JWT效验器
            Algorithm algorithm = Algorithm.HMAC256(key);
            JWTVerifier verifier = JWT.require(algorithm)
                    .build();
            //效验TOKEN(其实也就是比较两个token是否相同)
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 在token中获取到username信息
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 在token中获取到username信息
     */
    public static List getRole(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("roles").asList(String.class);
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 在token中获取到username信息
     */
    public static List getAccess(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("access").asList(String.class);
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 判断是否过期
     */
    public static boolean isExpire(String token) {
        DecodedJWT jwt = JWT.decode(token);
        return jwt.getExpiresAt().getTime() < System.currentTimeMillis();
    }
}
JwtToken类:
public class JwtToken implements AuthenticationToken {
    private String token;

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

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

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

}

4.新建获取用户信息的接口并且实现

public interface IUserService {
    String queryPasswordByName(String userName);

    RolePermModel queryRolePermByName(String userName);
}

        新建的model类用于用户权限信息(角色和权限)。

public class RolePermModel {

    private List roleName;

    private List permList;

    public List getPermList() {
        return Collections.unmodifiableList(permList);
    }
}

5.自定义一个Realm 

自定义的Realm需要继承AuthorizingRealm 类,我们需要重写以下两个方法。

doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。

@Slf4j
public class CustomerRealm extends AuthorizingRealm {

    @Autowired(required = false)
    IUserService iUserService;

    // 设置realm的名称
    @Override
    public void setName(String name) {
        super.setName("customRealm");
    }

    /**
     * 大坑!,必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }


    /**
     * 授权的方法
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("开始授权!");
        if (iUserService == null) {
            return null;
        }
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        RolePermModel rolePermModel = iUserService.queryRolePermByName(principalCollection.getPrimaryPrincipal().toString());
        simpleAuthorizationInfo.addRoles(rolePermModel.getRoleName());
        simpleAuthorizationInfo.addStringPermissions(rolePermModel.getPermList());
        return simpleAuthorizationInfo;
    }

    /**
     * 认证的方法
     *
     * @param authenticationToken
     * @return
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        log.info("开始认证!");
        if (iUserService == null) {
            log.warn("IUserService Is Empty");
            return null;
        }
        JwtToken jwtToken = (JwtToken) authenticationToken;
        String token = jwtToken.getCredentials().toString();
        String username = JwtUtil.getUsername(token);
        //toke过期
        if (JwtUtil.isExpire(token)) {
            throw new ExpiredCredentialsException();
        }
        if (!JwtUtil.verifyToken(token)) {
            throw new IncorrectCredentialsException();
        }

        List role = JwtUtil.getRole(token);
        List access = JwtUtil.getAccess(token);

        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                username, token, this.getName());
        return simpleAuthenticationInfo;
    }

    /**
     * 这里可以自定义密码比较器
     *
     * @param credentialsMatcher
     */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        super.setCredentialsMatcher(credentialsMatcher);
    }
}

6.自定义认证拦截的过滤器

自定义的过滤器需要继承AuthenticatingFilter,并且需要重写onAccessDenied方法,重写的方法必须执行SecurityUtils.getSubject().login才会执行我们CustomerRealm中的认证的方法(doGetAuthenticationInfo)

public class JwtAuthFilter extends AuthenticatingFilter {

    /**
     * 获取token
     *
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        String requestToken = getRequestToken((HttpServletRequest) servletRequest);
        JwtToken jwtToken = new JwtToken(requestToken);
        return jwtToken;
    }

    /**
     * 对跨域提供支持
     *
     * @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().equalsIgnoreCase("OPTIONS")) {
            httpServletResponse.setStatus(200);
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 验证token
     * 当访问拒绝时是否已经处理了;
     * 如果返回true表示需要继续处理;
     * 如果返回false表示该拦截器实例已经处理完成了,将直接返回即可。
     *
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //完成token登入
        //1.检查请求头中是否含有token
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String token = getRequestToken(httpServletRequest);
        //2. 如果客户端没有携带token,拦下请求
        if (null == token || "".equals(token)) {
            Utils.responseToken(servletResponse, "Token无效(authorization不存在)", HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        //3. 如果有,对进行进行token验证
        return this.executeLogin(servletRequest, servletResponse);
    }

    /**
     * 执行认证
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        JwtToken jwtToken = (JwtToken) createToken(request, response);
        try {
            SecurityUtils.getSubject().login(jwtToken);
        } catch (Exception e) {
            if (e.getClass().getName().equalsIgnoreCase(AuthenticationException.class.getName())) {
                Utils.responseToken(response, "Token无效,您无权访问该接口!", HttpStatus.UNAUTHORIZED.value());
            } else if (e.getClass().getName().equalsIgnoreCase(UnknownAccountException.class.getName())) {
                Utils.responseToken(response, "用户名不存在!", HttpStatus.SERVICE_UNAVAILABLE.value());
            } else if (e.getClass().getName().equalsIgnoreCase(IncorrectCredentialsException.class.getName())) {
                Utils.responseToken(response, "密码错误!", HttpStatus.SERVICE_UNAVAILABLE.value());
            } else if (e.getClass().getName().equalsIgnoreCase(ExpiredCredentialsException.class.getName())) {
                Utils.responseToken(response, "Token已过期!", HttpStatus.SERVICE_UNAVAILABLE.value());
            } else {
                Utils.responseToken(response, "其他错误!", HttpStatus.SERVICE_UNAVAILABLE.value());
            }
            return false;
        }
        return true;
    }

    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //从header中获取token
        String token = httpRequest.getHeader("authorization");

        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isEmpty(token)) {
            token = httpRequest.getParameter("authorization");
        }
        return token;
    }
}

Filter Chain 定义说明:

  • 1、一个URL可以配置多个 Filter,使用逗号分隔
  • 2、当设置多个过滤器时,全部验证通过,才视为通过
  • 3、部分过滤器可指定参数,如 perms,roles

Shiro 内置的 FilterChain

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter(所有 url 都都可以匿名访问)
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter(需要认证才能进行访问)
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter(需要权限访问)
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter(需要角色访问)
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter

 // /hello/add 必须认证,并且角色admin和addqueryrole,并且是add权限可以访问
 urlFilterMap.put("/hello/add", "authc,roles[admin,addqueryrole],perms[add]");
 // /hello/query 必须认证,并且角色admin或者addqueryrole,并且是add或者query权限可以访问
 // authc,roles,perms是shiro内置的过滤器,roles中括号是并且的关系
 // rolesOr和permsOr 是我们定义的过滤
 urlFilterMap.put("/hello/query", "authc,rolesOr[admin,addqueryrole],permsOr[add,query]");

7.自定义角色或关系过滤器



/**
 * 角色或过滤器
 */
public class CustomRolesOrFilter extends AuthorizationFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;
        List rolesList = CollectionUtils.asSet(rolesArray).stream().collect(Collectors.toList());
        for (String role : rolesList) {
            if (subject.hasRole(role)) {
                return true;
            }
        }
        return false;
    }
}

8.自定义权限或关系过滤器


/**
 * 权限或过滤器
 */
public class CustomPermissionsFilter extends AuthorizationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;
        List permsList = CollectionUtils.asSet(rolesArray).stream().collect(Collectors.toList());
        for (String perms : permsList) {
            if (subject.isPermitted(perms)) {
                return true;
            }
        }
        return false;
    }
}

9.新建一个Rest接口

        新建一个接口用于检验密码,登录成功返回token 

public class UserController extends MarkController {

    @Autowired(required = false)
    IUserService iUserService;

    @Autowired
    CredentialsMatcher credentialsMatcher;

    @GetMapping("/login")
    public String testCreateToken(@RequestParam String userName, @RequestParam String passWord) {
        //从数据库查询密码,
        String dbpassword= iUserService.queryPasswordByName(userName);
        if (userName == null || secret == null) {
            return "user is not exists!";
        }
        if (!dbpassword.equals(passWord)) {
            return "password is error!";
        }
        RolePermModel rolePermModel = iUserService.queryRolePermByName(userName);
        return JwtUtil.createJwtToken(userName, rolePermModel.getRoleName(), rolePermModel.getPermList());
    }
}

10.Shiro配置

IFilterService 是我自定新建的抽象类,用于上层抽象具体的业务,自定义过滤器和资源权限的注入。

@Configuration
public class ShiroAutoConfig {

    @Autowired(required = false)
    IFilterService iFilterService;

    //1创建shiroFilter  负责拦截请求
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        Map filterMap = new HashMap<>();
        filterMap.put("jwtfilter", new JwtAuthFilter());
        // 角色或关系
        filterMap.put("rolesOr", new CustomRolesOrFilter());
        // 权限或关系
        filterMap.put("permsOr", new CustomPermissionsoOrFilter());
        Map temp = iFilterService == null ? null : iFilterService.createFilterMap();
        if (temp != null) {
            filterMap.putAll(temp);
        }
        //自定义过滤器
        shiroFilterFactoryBean.setFilters(filterMap);
        //配置系统受限资源
        Map urlFilterMap = new LinkedHashMap<>();
        urlFilterMap.put("/login", "anon");
        // /hello/add 必须认证,并且角色admin和addqueryrole,并且是add权限可以访问
        urlFilterMap.put("/hello/add", "authc,roles[admin,addqueryrole],perms[add]");
        // /hello/query 必须认证,并且角色admin或者addqueryrole,并且是add或者query权限可以访问
        // authc,roles,perms是shiro内置的过滤器,roles中括号是并且的关系
        // rolesOr和permsOr 是我们定义的过滤
        urlFilterMap.put("/hello/query", "authc,rolesOr[admin,addqueryrole],permsOr[add,query]");
        urlFilterMap.put("/hello/delete", "rolesOr[admin,delrole],permsOr[delete]");
        // 对除了anon的过滤器,全部增加WT的过滤器
        urlFilterMap.entrySet().forEach(kv -> {
            if (kv.getValue().contains("anon")) {
                return;
            }
            if (!kv.getValue().contains("jwtfilter")) {
                kv.setValue("jwtfilter," + kv.getValue());
            }
        });
        // 全部接口使用JWT的过滤器
        urlFilterMap.put("/**", "jwtfilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(urlFilterMap);
        shiroFilterFactoryBean.setUnauthorizedUrl("/noAuthorization");
        return shiroFilterFactoryBean;
    }

    //2.创建安全管理器
    @Bean("securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("realm") Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //给安全管理器设置realm
        defaultWebSecurityManager.setRealm(realm);

        defaultWebSecurityManager.setSubjectDAO(defaultSubjectDAO());
        return defaultWebSecurityManager;
    }

    /**
     * 关闭shiro的session(无状态的方式使用shiro),shiro 默认带缓存,
     * 所以第一次认证之后就不会触发认证了
     *
     * @return
     */
    private DefaultSubjectDAO defaultSubjectDAO() {
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        return subjectDAO;
    }

    //3.创建自定义realm
    @Bean("realm")
    public Realm getRealm() {
        CustomerRealm customerRealm = new CustomerRealm();
        return customerRealm;
    }


    @Bean
    @ConditionalOnClass(RestController.class)
    public UserController tokenController() {
        UserController tokenController = new UserController();
        return tokenController;
    }

    @Bean
    @ConditionalOnClass(RestController.class)
    public ExceptionController exceptionController() {
        ExceptionController exceptionController = new ExceptionController();
        return exceptionController;
    }

    @Bean
    HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //加密类型(+)
        hashedCredentialsMatcher.setHashAlgorithmName("sha-256");
        //#加密迭代次数(+)
        hashedCredentialsMatcher.setHashIterations(1000);
        //#true=hex格式  false=base64(+)
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(false);
        return hashedCredentialsMatcher;
    }

}

11.设置全局异常捕获权限异常

package com.lx.shirojwt.controller;

import com.lx.shirojwt.util.ShiroConstants;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

@RestController
@RestControllerAdvice
public class ExceptionController extends MarkController {

    @GetMapping("/noAuthorization")
    public ResponseEntity noAuthorization(HttpServletRequest request) {
        return new ResponseEntity("无权限访问该接口!", HttpStatus.FORBIDDEN);
    }


    @ExceptionHandler({AuthorizationException.class})
    public ResponseEntity authorizationException(Exception exception, HttpServletRequest request) {
        return new ResponseEntity("无权限访问!" + request.getRequestURI(), HttpStatus.FORBIDDEN);
    }

}

12.启动测试

无token字段:

springboot shiro jwt实现认证和授权_第1张图片

token过期:

springboot shiro jwt实现认证和授权_第2张图片 正常token:

springboot shiro jwt实现认证和授权_第3张图片

总结

       1.对于JWT每次请求都会进行认证,首先先验证token的有效期,然后校验token的合法性,最后解析token获取用户信息。

        2.目前没有使用缓存,每次鉴权都要去数据库查Role、查Permissions。

你可能感兴趣的:(Shiro授权认证,spring,boot,java,spring)