第九篇:SpringBoot整合Shiro+Jwt

Shiro安全认证框架

Shiro 概述

Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架,使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。

Shiro 认证流程

在这里插入图片描述
具体流程分析如下:

  1. 首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecurityManager()设置;
  2. SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;
  3. Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;
  4. Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证;
  5. Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
Jwt
  1. 什么是JWT
    JWT(Json Web Token),是一种工具,格式为XXXX.XXXX.XXXX的字符串,JWT以一种安全的方式在用户和服务器之间传递存放在JWT中的不敏感信息。

  2. 为什么要用JWT
    设想这样一个场景,在我们登录一个网站之后,再把网页或者浏览器关闭,下一次打开网页的时候可能显示的还是登录的状态,不需要再次进行登录操作,通过JWT就可以实现这样一个用户认证的功能。当然使用Session可以实现这个功能,但是使用Session的同时也会增加服务器的存储压力,而JWT是将存储的压力分布到各个客户端机器上,从而减轻服务器的压力。

  3. JWT长什么样子?
    JWT由3个子字符串组成,分别为Header,Payload以及Signature,结合JWT的格式即:Header.Payload.Signature。(Claim是描述Json的信息的一个Json,将Claim转码之后生成Payload)。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODY1MTA3MTIsInVzZXJuYW1lIjoiZGV2aW4ifQ.9MHYCpJCfKBbaDcJV4NUneL8OM9BdNaNrLgGW3Nznxc
Shiro + Jwt 应用实践
  1. pom.xml 添加 Shiro 和 Jwt 依赖
		<!-- 整合shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        
        <!-- jwt依赖 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
  1. 编写 JwtUtil 工具类
@Component
public class JwtUtil {


    /**
     * 失效时间
     */
    private static final long EXPIRE_TIME = 5 * 60 * 1000;

    /**
     * 生成token 5分钟后过期
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param username
     * @return java.lang.String
     */
    public static String sign(String username, String secret){
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            return JWT.create()
                    //payload 载荷
                    .withClaim("username", username)
                    //设置过期时间
                    .withExpiresAt(date)
                    //创建一个新的JWT,并使用给定的算法进行标记
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }

    }

    /**
     * 校验token是否正确
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param token
     * @param userName 
     * @return boolean
     */
    public static boolean validateToken(String token, String userName, String secret){
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            //在token中附带了username信息
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", userName)
                    .build();
            //验证token
            verifier.verify(token);
            return true;
        } catch (Exception e) {
            return false;
        }

    }

    /**
     * 获取token中的信息无需secretKey也能获取
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param token 
     * @return java.lang.String
     */
    public static String getUserName(String token){
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}
  1. 编写 JwtToken 实现 AuthenticationToken 接口
public class JwtToken implements AuthenticationToken {

    private static final long serialVersionUID = 2334863906091691445L;
    /**
     * 秘钥
     */
    private String token;


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


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

    @Override
    public Object getCredentials() {
        return token;
    }
}
  1. 自定义 shiro 的过滤器,不要使用 shiro 默认的过滤器
public class JwtShiroFilter extends BasicHttpAuthenticationFilter {
    private final Logger log = LoggerFactory.getLogger(JwtShiroFilter.class);

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 如果带有 token,则对 token 进行检查,否则直接通过
     * @param request
     * @param response
     * @param mappedValue
     * @return
     * @throws UnauthorizedException
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                //非法请求
                responseError(response, e.getMessage());
            }
        }
        //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
        return true;
    }

    /**
     * 判断用户是否想要登入
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param request
     * @param response 
     * @return boolean
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest)request;
        String authorization = httpServletRequest.getHeader("Authorization");   
        return authorization != null;
    }

    /**
     * 执行登陆操作
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception{
        HttpServletRequest httpServletRequest = (HttpServletRequest)request;
        String authorization = httpServletRequest.getHeader("Authorization");
        JwtToken token = new JwtToken(authorization);

        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;

    }


    /**
     * 对跨域提供支持
     */
    @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 responseError(ServletResponse response, String message) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            //设置编码,否则中文字符在重定向时会变为空字符串
            message = URLEncoder.encode(message, "UTF-8");
            httpServletResponse.sendRedirect("/unauthorized/" + message);
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}
  1. 自定义 JwtShiroRealm 继承 AuthorizingRealm 实现认证与授权
/**
 * 监视授权信息
 * @ClassName ShiroUserRealm
 * @Author 药岩
 * @Date 2020/2/10
 * Version 1.0
 */
public class JwtShiroRealm extends AuthorizingRealm {

    @Autowired
    private AuthorityDao authorityDao;

    @Autowired
    private LoginDao loginDao;

    @Autowired
    private LoginServer loginServer;

    public void setLoginServer(LoginServer loginServer){
        this.loginServer = loginServer;
    }

    /**
     * 判断token是否事我们的这个jwttoekn
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 认证
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param authcToken
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
      
        //0. token中存储着输入的用户名
        String token = (String) auth.getCredentials();

        //1. token解密获取username
        String userName = JwtUtil.getUserName(token);
        if (userName == null){
            throw new AuthenticationException("token令牌有误");
        }

        //2.根据用户名去数据库查找是否存在该用户
        Users users = loginDao.selectUserData(userName);

        if(users == null){
            throw new AuthenticationException("用户不存在");
        }

        //3.判断token令牌是否有误
        if (!JwtUtil.validateToken(token, userName, users.getPassword())){
            throw new AuthenticationException("token令牌过期失效");
        }

        return new SimpleAuthenticationInfo(token, token, "jwtRealm");
    }


    /**
     * 授权
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param principals 
     * @return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1.获取用户信息
        String userName = JwtUtil.getUserName(principals.toString());

        //2.根据用户名查找该用户
        Users users = loginDao.selectUserData(userName);

        //2.根据用户信息-->角色信息-->获取资源的访问权陿
        List<String> menus = authorityDao.findUserPermissions(users.getId());
        //3.根据用户信息直接获取访问权限
        List<String> userMenu = authorityDao.findUserMenuPermissions(users.getId());
        //查询角色
        List<String> role = authorityDao.findRoleByUserId(users.getId());
        List<String> list = new ArrayList<>();
        if(menus!=null){
            list.addAll(menus);
        }
        if(userMenu!=null){
            list.addAll(userMenu);
        }
        //4.去重权限
        Set<String> set=new HashSet<>(list);
        Set<String> setRole = new HashSet<>(role);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(set);

        info.setRoles(setRole);
        return info;
    }

}
  1. 编写 Shiro 的配置类 ShiroConfig
/**
 * Shiro 的配置类
 * @ClassName ShiroConfig
 * @Author 药岩
 * @Date 2020/2/10
 * Version 1.0
 */

@Configuration
public class ShiroConfig {

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param
     * @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        autoProxyCreator.setProxyTargetClass(true);
        return autoProxyCreator;
    }


    /**
     * 开启aop注解支持
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param securityManager
     * @return org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 自定义shiro
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param
     * @return com.app.common.ShiroUserRealm
     */
    @Bean("jwtRealm")
    public JwtShiroRealm shiroUserRealm(){
        JwtShiroRealm userRealm = new JwtShiroRealm();
        return userRealm;
    }

    /**
     * 注入SecurityManager
     * 配置shiro安全管理器,是shiro框架的核心安全管理器
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param
     * @return org.apache.shiro.mgt.SecurityManager
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(shiroUserRealm());

        //关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);
        return defaultWebSecurityManager;
    }

    /**
     * shiroFilter 的工厂配置
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param securityManager
     * @return org.apache.shiro.spring.web.ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //添加自定义shiro的拦截器
        Map<String, Filter> jwtFilter = new HashMap<>();
        jwtFilter.put("jwt", new JwtShiroFilter());
        shiroFilterFactoryBean.setFilters(jwtFilter);

        //必须设置 securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //设置无权限时跳转的URL
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized/无权限");

        //自定义URL
        Map<String, String> map = new HashMap<>();
        map.put("/user/doLogin", "anon");
        map.put("/user/logout", "logout");
        map.put("/swagger-ui.html", "anon");
        map.put("/swagger-resources/**", "anon");
        map.put("/unauthorized/**", "anon");
        map.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;

    }

}
  1. 添加全局异常处理类
/**
 * 全局异常处理
 * @ClassName GlobalExceptionHandler
 * @Author 药岩
 * @Date 2020/4/10
 * Version 1.0
 */

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理shiro抛出的异常
     * @author 药岩
     * @date 2020/2/10
     * @param  * @param e
     * @return com.app.common.CommonResult
     */

    @ExceptionHandler(ShiroException.class)
    public CommonResult handleShiroException(Exception e){
        log.error("shiro error[{}]", e.getMessage());
        return CommonResult.failed(401, "您没有权限访问", new int[]{});
    }

    @ExceptionHandler(Exception.class)
    public CommonResult handleException(HttpServletRequest request, Throwable e){
        log.error("系统访问异常error[{}]", e.getMessage());
        return CommonResult.failed(getStatus(request).value(), "访问出错,无法访问:" + e.getMessage(), new int[]{});
    }

    public HttpStatus getStatus(HttpServletRequest request){
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null){
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }


}
  1. controller 层用户登入认证
/**
 * 用户登入验证
 * @ClassName LoginController
 * @Author 药岩
 * @Date 2020/2/10
 * Version 1.0
 */

@RestController
@RequestMapping(value = "/user")
@Api(tags = "LoginController", description = "用户登入验证")
@Slf4j
public class LoginController {

    @Autowired
    private LoginServer loginServer;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 用于用户登入验证
     * @param
     * @param
     * @return
     */
    @PostMapping(value = "/doLogin")
    public CommonResult login(@RequestBody Users usersLogin,
                              @RequestParam(required=true,value="checked",defaultValue="") String checked,  HttpServletResponse response, HttpServletRequest request) throws UnauthorizedException {
		//通过用户名
        Users users = loginServer.selectUserData(usersLogin.getLoginName());
        if (users == null){
            return CommonResult.failed("用户不存在", new Object[]{});
        }
        usersLogin.setPassword(DigestUtils.md5DigestAsHex(usersLogin.getPassword().getBytes()));
        if (users.getPassword().equals(usersLogin.getPassword())){
            String token = JwtUtil.sign(usersLogin.getLoginName(), usersLogin.getPassword());          
            response.setHeader("Authorization", token);
            log.info("用户[{}]登入成功,生成token[{}]", users.getLoginName(), token);
            return CommonResult.success(token, "登入成功");
        }else {
            return CommonResult.failed("用户名密码错误", new Object[]{});
        }

    }

    /**
     * 登出操作
     */
    @RequestMapping("logout")
    public CommonResult logout(HttpServletRequest request){
        SecurityUtils.getSubject().logout();
        redisUtil.del(request.getHeader("Authorization"));
        return CommonResult.success(new Object[]{}, "登出成功");
    }
}
  1. Postman 测试
    用户登入
    在这里插入图片描述
    携带 token 访问接口资源
    在这里插入图片描述
发布了29 篇原创文章 · 获赞 43 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/CSDN_Qiang17/article/details/104803992
今日推荐