shiro框架入门实践——加入JWT做一个登录验证

本博文代码:https://download.csdn.net/download/qq_39404258/12439869

基本概念从其他博文中看,此处不讲。

该项目使用了springboot、mybaits-plus、jwt、shiro、redis。mybaits-plus基本没用,只做了一次数据库查询,redis暂时不使用,登录验证成功后再追加redis操作。

先说一下大致思路:

登录操作:访问登录接口-》通过数据库判断是否存在-》存在后,进行shiro登录-》将登录信息转化为jwt的token返回。

验证操作:访问验证接口-》通过过滤器拦截此请求-》将传入的token拿去shiro登录(转入自定义的realm处理)-》token解析正确验证成功。

pom文件:有些可能没用,按需索取

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!--日志  -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!-- mp 依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
            <classifier>jdk15</classifier>
        </dependency>
        <!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.4.1</version>
        </dependency>
        <!--jedis-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

        <!--引入JWT依赖,由于是基于Java,所以需要的是java-jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
    </dependencies>

一些配置:

swagger

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        ParameterBuilder tokenPar = new ParameterBuilder();
        List<Parameter> pars = new ArrayList<Parameter>();
        tokenPar.name("Authorization").description("Authorization")
                .modelRef(new ModelRef("string")).parameterType("header").required(false).build();
        pars.add(tokenPar.build());
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("mptest.mybatistest"))
                .paths(PathSelectors.any())
                .build().globalOperationParameters(pars)  ;
    }

    @SuppressWarnings("deprecation")
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("个人测试")
                .description("个人测试用api")
                .termsOfServiceUrl("termsOfServiceUrl")
                .contact("测试")
                .version("1.0")
                .build();
    }

}

重点配置shiro

@Configuration
public class ShiroConfig {

    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //设置过滤器
        Map<String, Filter> filtersMap = shiroFilterFactoryBean.getFilters();
        filtersMap.put("jwt", new ShiroJWTFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);
        // shiro内置过滤器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        //swagger接口权限 开放
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon"); //swagger
        filterChainDefinitionMap.put("/user/login", "anon");//后台登录
        filterChainDefinitionMap.put("/**", "jwt");  //jwt token验证
        //默认登陆页面
        //shiroFilterFactoryBean.setLoginUrl("/user/login");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean("authenticator")
    public Authenticator authenticator(){
        ModularRealmAuthenticator authenticator = new UserModularRealmAuthenticator();
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return authenticator;
    }
    @Bean("authorizer")
    public Authorizer authorizer() {
        Authorizer modularRealmAuthorizer = new ModularRealmAuthorizer();
        Collection<Realm> realmCollection = new HashSet<>();
        realmCollection.add(new UserRealm());
        realmCollection.add(new TokenInvalidRealm());
        ((ModularRealmAuthorizer) modularRealmAuthorizer).setRealms(realmCollection);

        return modularRealmAuthorizer;
    }
    @Bean(name="userRealm")
    public UserRealm userRealm() {
        return new UserRealm();
    }
    @Bean
    public TokenInvalidRealm tokenInvalidRealm() {
        return new TokenInvalidRealm();
    }
    @Bean(name="defaultWebSecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
        // 设置realm.

        defaultWebSecurityManager.setAuthorizer(authorizer());
        defaultWebSecurityManager.setAuthenticator(authenticator());
        defaultWebSecurityManager.setRealms(Arrays.asList(userRealm(),tokenInvalidRealm()));
        return defaultWebSecurityManager;
    }


}

来分析一下配置:

首页去掉swagger的过滤(前四个),去掉登录接口的过滤,其他所有接口都放入jwt过滤器,也就是自定义的ShiroJWTFilter(在访问验证时介绍)

 // shiro内置过滤器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        //swagger接口权限 开放
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon"); //swagger
        filterChainDefinitionMap.put("/user/login", "anon");//后台登录
        filterChainDefinitionMap.put("/**", "jwt");  //jwt token验证

又因为配置的是多realm(一个用来登录逻辑、一个用来验证逻辑),要配置一个 ModularRealmAuthenticator

去处理请求时的realm,我们来自己写一个子类来处理。

/**
  用于过滤该走哪些realm
 */
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {

    private static final Logger logger = LoggerFactory.getLogger(UserModularRealmAuthenticator.class);

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException {

//        logger.info("UserModularRealmAuthenticator:method doAuthenticate() execute ");

        // 判断getRealms()是否返回为空
        assertRealmsConfigured();

        // 所有Realm
        Collection<Realm> realms = getRealms();

        // 过滤, 根据 是否支持 判断
        List<Realm> lastReam = realms.stream().filter(current -> current.supports(authenticationToken)).collect(Collectors.toList());

        if (CollectionUtils.isEmpty(lastReam)) {
            Assert.notEmpty(lastReam, "realms is empty");
        }

        if (lastReam.size() == 1) {
            return doSingleRealmAuthentication(lastReam.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(lastReam, authenticationToken);
        }
    }

}

这段代码核心就这一个:

 拿到所有realm后,将符合条件的放入集合,按顺序处理realm逻辑,这样就没必要每次请求都走一遍所有的realm。

  Collection<Realm> realms = getRealms();

        // 过滤, 根据 是否支持 判断
        List<Realm> lastReam = realms.stream().filter(current -> current.supports(authenticationToken)).collect(Collectors.toList());

这里有一个坑,如果setAuthenticator在realm后面会出现这样的错

Configuration error: No realms have been configured! One or more realms must be present to execute an authentication attempt.

解决办法:将setRealms放在setAuthorizer后面,先配置authorizer,再配置realm。

原因请参考博文:https://blog.csdn.net/u011833033/article/details/104018407

  defaultWebSecurityManager.setAuthenticator(authenticator());
        defaultWebSecurityManager.setRealms(Arrays.asList(userRealm(),tokenInvalidRealm()));

然后我们看一下两个realm:

UserRealm:在doGetAuthenticationInfo里进行了一次登录操作

//AuthenticatingRealm只用做身份验证     |AuthorizingRealm 用作身份验证和权限验证
public class UserRealm extends AuthenticatingRealm {
	@Autowired
	private UserDao userDao;

	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof UserToken;
	}
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("执行登录认证方法!");
		UsernamePasswordToken usertoken=(UsernamePasswordToken) token;
		User user = userDao.selectOne(new QueryWrapper<User>().eq("username",usertoken.getUsername()));
		if (user==null) {
			System.out.println("验证错误,抛出异常!");
			return null;
		}
		System.out.println("执行doGetAuthenticationInfo认证方法完毕!");
		//第一个参数userInfo对象对用的用户名,第二个参数,传的是获取的password
		return new SimpleAuthenticationInfo(usertoken.getUsername(),usertoken.getPassword(),"");
	}

}

 TokenInvalidRealm :在doGetAuthenticationInfo进行了一次登录操作

public class TokenInvalidRealm extends AuthorizingRealm {
    @Autowired
    private UserDao userDao;
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行登录认证方法!");
        JWTToken usertoken=(JWTToken) authenticationToken;
        String token = usertoken.getToken();
        DecodedJWT jwt = JWT.decode(token);
        String username = jwt.getClaim("username").asString();
        User user = userDao.selectOne(new QueryWrapper<User>().eq("username",username));
        if (user==null) {
            System.out.println("验证错误,抛出异常!");
            return null;
        }
        System.out.println("执行doGetAuthenticationInfo认证方法完毕!");
        return new SimpleAuthenticationInfo(user,token,TokenInvalidRealm.class.getName());

    }
}

然后这两个里面都有一个supports方法,用于判断传入的token类型,来判断请求接口时用哪些realm进行逻辑处理。

重写的两个token类型:

/*
用作 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;
    }

    public String getToken(){
        return token;
    }
}
/*
shiro token验证环境
 */
public class UserToken extends UsernamePasswordToken {

    public UserToken(String username, String password) {
        super(username, password);
    }

}

配置说完了,然后来写controller,因为springboot、mybatis不是重点,所以不作介绍,省略dao、service层。

shiro框架中 执行这行代码时subject.login(token);会进入realm的doGetAuthenticationInfo方法中。

@RestController
@RequestMapping("user")
public class LoginController {

    //过期时间
    private static long time=1000*60;

    @Autowired
    private UserDao userDao;

    @PostMapping("/login")
    public String login(String password,String username){
        User user = userDao.selectOne(new QueryWrapper<User>().eq("username",username).eq("password",password));
        if (user==null){
            return "账号或密码错误";
        }
        UserToken token = new UserToken(username,password);
       Subject subject =  SecurityUtils.getSubject();
       try {
           subject.login(token);
           //指定签名算法,header部分
           Algorithm algorithm=Algorithm.HMAC256(password.getBytes(StandardCharsets.UTF_8));
           Date expire=new Date(System.currentTimeMillis()+time);
           //jwt token签证
           String authorization = JWT.create().withClaim("username",username).withExpiresAt(expire).sign(algorithm);
           return authorization;
       }catch (Exception e){
           return "登录验证失败";
       }
    }

    @GetMapping("/shiroJWT")
    public String shiroJWT(){
        return "shiroJWT验证成功";
    }
}

我们访问登录接口,结果账号密码正确时返回jwt token

 然后接着我们访问验证接口

因为我们自定义的过滤器ShiroJWTFilter,这个请求符合过滤器规则,则进入该过滤器。

首页会进入isAccessAllowed方法,然后执行executeLogin方法,将获得的token进行login操作,因为这个token是JWTToken 类型的,接着跳转到TokenInvalidRealm的doGetAuthenticationInfo方法进行逻辑操作。如果验证失败则抛出异常结束,正确则进入controller进行逻辑执行。

public class ShiroJWTFilter extends AuthenticatingFilter {

    public ShiroJWTFilter(){
        this.setLoginUrl("/user/login");
    }
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");
        JWTToken token=new JWTToken(authorization);
        //因为login()方法里得参数是AuthenticationToken,需将jwt签证转换为AuthenticationToken
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        return null;
    }
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if(this.isLoginRequest(request, response)){
            return true;
        }else {
            boolean allowed = false;
            try {
                allowed = executeLogin(request, response);
            } catch (Exception e) {
                System.out.println("失败了");
            }
            return allowed || super.isPermissive(mappedValue);
        }

    }
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
        String json="{\"code\":401,\"message\":\"token validation fails\"}";
        httpServletResponse.setHeader("Content-type", "application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(json);
        return false;
    }
}

运行结果:

这样,整个shiro和jwt整合就完成了。

本质上就是拿到token以后,之后的每个请求都要携带token,而每次请求其实都重新做了一次登录操作。

如果每次都查一下数据库则效率会降低,那么我们第一次登录时就将用户信息存入缓存,之后每次拿只需要去缓存中去取。

增加了redis工具类:

public class JedisUtil {
    private static JedisPool jp;
    static {
        JedisPoolConfig jpc=new JedisPoolConfig();
        jpc.setMaxIdle(10);//最大空闲
        jpc.setMaxTotal(30);//最大连接
         jp= new JedisPool(jpc,"127.0.0.1",6379);
    }
    public static Jedis getJedis(){
        return jp.getResource();
    }
}

修改controller

@PostMapping("/login")
    public String login(String password,String username){
        User user = userDao.selectOne(new QueryWrapper<User>().eq("username",username).eq("password",password));
        if (user==null){
            return "账号或密码错误";
        }
        Jedis jedis= JedisUtil.getJedis();
        jedis.set(username,password);
        UserToken token = new UserToken(username,password);
       Subject subject =  SecurityUtils.getSubject();
       try {
           subject.login(token);
           //指定签名算法,header部分
           Algorithm algorithm=Algorithm.HMAC256(password.getBytes(StandardCharsets.UTF_8));
           Date expire=new Date(System.currentTimeMillis()+time);
           //jwt token签证
           String authorization = JWT.create().withClaim("username",username).withExpiresAt(expire).sign(algorithm);
           return authorization;
       }catch (Exception e){
           return "登录验证失败";
       }
    }

修改验证的realm

 @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行登录认证方法!");
        JWTToken usertoken=(JWTToken) authenticationToken;
        String token = usertoken.getToken();
        DecodedJWT jwt = JWT.decode(token);
        String username = jwt.getClaim("username").asString();
        Jedis jedis = JedisUtil.getJedis();
        String pawd = jedis.get(username);
        User user=null;
        if (pawd==null){
            user = userDao.selectOne(new QueryWrapper<User>().eq("username",username));
            if (user==null) {
                System.out.println("验证错误,抛出异常!");
                return null;
            }
            jedis.set(user.getUsername(),user.getPassword());

            System.out.println("执行doGetAuthenticationInfo认证方法完毕!");
            //都是SimpleAuthenticationInfo为什么两次传的不一样
            return new SimpleAuthenticationInfo(user,token,TokenInvalidRealm.class.getName());
        }else {
            System.out.println("执行doGetAuthenticationInfo认证方法完毕!");
            //都是SimpleAuthenticationInfo为什么两次传的不一样
            return new SimpleAuthenticationInfo(username,token,TokenInvalidRealm.class.getName());
        }


    }

猜你喜欢

转载自blog.csdn.net/qq_39404258/article/details/106230358
今日推荐