springboot整合shiro實現認證

                   
  •  
  •  
Part2 今日主題:springboot整合shiro

1簡介

shiro是一款安全框架,能夠控制登陸,能夠保證安全,對於咱們來講一些接口的安全必須經過安全框架來控制,防止別人蓄意刷接口。前端

2原理

我仍是要講一下他的原理吧,若是這我的沒有通過登陸頁面,去訪問其餘頁面,shiro框架會將請求轉發到登陸頁面去,讓這我的登陸,登陸成功以後會給前端一個token,前端將token保存下來,每次去請求項目中的其餘頁面或者接口的時候,須要將token攜帶到請求頭中,給後端,請求首先會通過攔截器,攔截器會對token進行判斷,判斷該token是否有效,若是有效,則放行,不然直接攔截了。java

3環境

  • springboot

4依賴

咱們須要shiro和jwt的依賴web

 <!-- 自動依賴導入 shiro-core 和 shiro-web -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.1</version>
        </dependency>
        <!--jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.11.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

這裏用到jwt,簡單的理解一下,他就是一款加密工具,登陸成功以後,咱們須要將一些信息發給前端,可是咱們不可能明文發送,須要對這些信息進行加密,這個token就是一些加密的信息。算法

5實現過程

1.首先寫一個jwt加密解密的工具類spring

/*
 * 總的來講,工具類中有三個方法
 * 獲取JwtToken,獲取JwtToken中封裝的信息,判斷JwtToken是否存在
 * 1. encode(),參數是=簽發人,存在時間,一些其餘的信息=。返回值是JwtToken對應的字符串
 * 2. decode(),參數是=JwtToken=。返回值是荷載部分的鍵值對
 * 3. isVerify(),參數是=JwtToken=。返回值是這個JwtToken是否存在
 * */
public class JwtUtil {
    //建立默認的祕鑰和算法,供無參的構造方法使用
    private static final String defaultbase64EncodedSecretKey = "java後端指南";
    private static final SignatureAlgorithm defaultsignatureAlgorithm = SignatureAlgorithm.HS256;

    public JwtUtil() {
        this(defaultbase64EncodedSecretKey, defaultsignatureAlgorithm);
    }

    private final String base64EncodedSecretKey;
    private final SignatureAlgorithm signatureAlgorithm;

    public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
        this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
        this.signatureAlgorithm = signatureAlgorithm;
    }

    /*
     *這裏就是產生jwt字符串的地方
     * jwt字符串包括三個部分
     *  1. header
     *      -當前字符串的類型,通常都是「JWT」
     *      -哪一種算法加密,「HS256」或者其餘的加密算法
     *      因此通常都是固定的,沒有什麼變化
     *  2. payload
     *      通常有四個最多見的標準字段(下面有)
     *      iat:簽發時間,也就是這個jwt何時生成的
     *      jti:JWT的惟一標識
     *      iss:簽發人,通常都是username或者userId
     *      exp:過時時間
     *
     * */
    public String encode(String iss, long ttlMillis, Map<String, Object> claims) {
        //iss簽發人,ttlMillis生存時間,claims是指還想要在jwt中存儲的一些非隱私信息
        if (claims == null) {
            claims = new HashMap<>();
        }
        long nowMillis = System.currentTimeMillis();

        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)
                .setId(UUID.randomUUID().toString())//2. 這個是JWT的惟一標識,通常設置成惟一的,這個方法能夠生成惟一標識
                .setIssuedAt(new Date(nowMillis))//1. 這個地方就是以毫秒爲單位,換算當前系統時間生成的iat
                .setSubject(iss)//3. 簽發人,也就是JWT是給誰的(邏輯上通常都是username或者userId)
                .signWith(signatureAlgorithm, base64EncodedSecretKey);//這個地方是生成jwt使用的算法和祕鑰
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);//4. 過時時間,這個也是使用毫秒生成的,使用當前時間+前面傳入的持續時間生成
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    //至關於encode的方向,傳入jwtToken生成對應的username和password等字段。Claim就是一個map
    //也就是拿到荷載部分全部的鍵值對
    public Claims decode(String jwtToken) {

        // 獲得 DefaultJwtParser
        return Jwts.parser()
                // 設置簽名的祕鑰
                .setSigningKey(base64EncodedSecretKey)
                // 設置須要解析的 jwt
                .parseClaimsJws(jwtToken)
                .getBody();
    }

    //判斷jwtToken是否合法
    public boolean isVerify(String jwtToken) {
        //這個是官方的校驗規則,這裏只寫了一個」校驗算法「,能夠本身加
        Algorithm algorithm = null;
        switch (signatureAlgorithm) {
            case HS256:
                algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
                break;
            default:
                throw new RuntimeException("不支持該算法");
        }
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(jwtToken);  // 校驗不經過會拋出異常
        //判斷合法的標準:1. 頭部和荷載部分沒有篡改過。2. 沒有過時
        return true;
    }

2.類型UsernamePasswordToken的類數據庫

/**
 * 這個就相似UsernamePasswordToken
 */
public class JwtToken implements AuthenticationToken {
    //返回值都是jwt
    
    private String jwt;
    
    public JwtToken(String jwt) {
        this.jwt = jwt;
    }

    /**
     * 相似是用戶名
     * @return
     */
    @Override
    public Object getPrincipal() {
        return jwt;
    }

    /**
     * 相似密碼
     * @return
     */
    @Override
    public Object getCredentials() {
        return jwt;
    }
   
}

3.建立JwtDefaultSubjectFactory,來實現不保存sessionapache

public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        // 不建立 session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

4.建立AuthorizingRealmjson

@Slf4j
public class JwtRealm extends AuthorizingRealm {
    /*
     * 多重寫一個support
     * 標識這個Realm是專門用來驗證JwtToken
     * 不負責驗證其餘的token(UsernamePasswordToken)
     * */
    @Override
    public boolean supports(AuthenticationToken token) {
        //這個token就是從過濾器中傳入的jwtToken
        return token instanceof JwtToken;
    }

    //受權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    //認證
    //這個token就是從過濾器中傳入的jwtToken
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        String jwt = (String) token.getPrincipal();
        if (jwt == null) {
            throw new NullPointerException("jwtToken 不容許爲空");
        }
        //判斷
        JwtUtil jwtUtil = new JwtUtil();
        if (!jwtUtil.isVerify(jwt)) {
            throw new UnknownAccountException();
        }
        //下面是驗證這個user是不是真實存在的
        String username = (String) jwtUtil.decode(jwt).get("username");//判斷數據庫中username是否存在
        log.info("在使用token登陸"+username);
        return new SimpleAuthenticationInfo(jwt,jwt,"JwtRealm");
        //這裏返回的是相似帳號密碼的東西,可是jwtToken都是jwt字符串。還須要一個該Realm的類名

    }

}

5.實現過濾器AccessControlFilter後端

@Slf4j
public class JwtFilter extends AccessControlFilter {
    /*
     * 1. 返回true,shiro就直接容許訪問url
     * 2. 返回false,shiro纔會根據onAccessDenied的方法的返回值決定是否容許訪問url
     * */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        log.warn("isAccessAllowed 方法被調用");
        //這裏先讓它始終返回false來使用onAccessDenied()方法
        return false;
    }

    /**
     * 返回結果爲true代表登陸經過
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        log.warn("onAccessDenied 方法被調用");
        //這個地方和前端約定,要求前端將jwtToken放在請求的Header部分

        //因此之後發起請求的時候就須要在Header中放一個Authorization,值就是對應的Token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        log.info("請求的 Header 中藏有 jwtToken {}", jwt);
        JwtToken jwtToken = new JwtToken(jwt);
        /*
         * 下面就是固定寫法
         * */
        try {
            // 委託 realm 進行登陸認證
            //因此這個地方最終仍是調用JwtRealm進行的認證
            getSubject(servletRequest, servletResponse).login(jwtToken);
            //也就是subject.login(token)
        } catch (Exception e) {
            e.printStackTrace();
            onLoginFail(servletResponse);
            //調用下面的方法向客戶端返回錯誤信息
            return false;
        }

        return true;
        //執行方法中沒有拋出異常就表示登陸成功
    }

    //登陸失敗時默認返回 401 狀態碼
    private void onLoginFail(ServletResponse response) throws IOException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.getWriter().write("login error");
    }
}

6.添加配置文件,實現某些路徑的認證和受權安全

//springBoot整合jwt實現認證有三個不同的地方,對應下面abc
@Configuration
public class ShiroConfig {
    /*
     * a. 告訴shiro不要使用默認的DefaultSubject建立對象,由於不能建立Session
     * */
    @Bean
    public SubjectFactory subjectFactory() {
        return new JwtDefaultSubjectFactory();
    }

    @Bean
    public Realm realm() {
        return new JwtRealm();
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        /*
         * b
         * */
        // 關閉 ShiroDAO 功能
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 不須要將 Shiro Session 中的東西存到任何地方(包括 Http Session 中)
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        //禁止Subject的getSession方法
        securityManager.setSubjectFactory(subjectFactory());
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager());
        shiroFilter.setLoginUrl("/unauthenticated");
        shiroFilter.setUnauthorizedUrl("/unauthorized");
        /*
         * c. 添加jwt過濾器,並在下面註冊
         * 也就是將jwtFilter註冊到shiro的Filter中
         * 指定除了login和logout以外的請求都先通過jwtFilter
         * */
        Map<String, Filter> filterMap = new HashMap<>();
        //這個地方其實另外兩個filter能夠不設置,默認就是
        filterMap.put("anon", new AnonymousFilter());
        filterMap.put("jwt", new JwtFilter());
        filterMap.put("logout", new LogoutFilter());
        shiroFilter.setFilters(filterMap);

        // 攔截器
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        filterRuleMap.put("/login""anon");
        filterRuleMap.put("/logout""logout");
        filterRuleMap.put("/**""jwt");
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);

        return shiroFilter;
    }
}

7.實現控制類

我這裏爲了省事,直接用的是一個固定的用戶名和密碼,你們能夠本身寫一個方法去數據庫查詢

   @RequestMapping("/login")
    public ResponseEntity<Map<String, String>> login(String username, String password) {
        log.info("username:{},password:{}",username,password);
        Map<String, String> map = new HashMap<>();
        if (!"tom".equals(username) || !"123".equals(password)) {
            map.put("msg""用戶名密碼錯誤");
            return ResponseEntity.ok(map);
        }
        JwtUtil jwtUtil = new JwtUtil();
        Map<String, Object> chaim = new HashMap<>();
        chaim.put("username", username);
        String jwtToken = jwtUtil.encode(username, 5 * 60 * 1000, chaim);
        map.put("msg""登陸成功");
        map.put("token", jwtToken);
        return ResponseEntity.ok(map);
    }
    @RequestMapping("/testdemo")
    public ResponseEntity<String> testdemo() {
        return ResponseEntity.ok("java後端指南");
    }

8.訪問 若是直接訪問其餘接口,可是沒有登陸的話就會報沒有登陸watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

首先是登陸成功,返給前端一個tokenwatermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=而後咱們再攜帶token到後端去,訪問其餘接口

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=
                                                                                                       
                                                                                           

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

相關文章
相關標籤/搜索