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());
        }


    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章