SpringBoot 整合 Shiro 密碼登入與郵件驗證碼登入(多 Realm 認證)

風青宇發表於2021-02-25
  • 匯入依賴(pom.xml) 

        <!--整合Shiro安全框架-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!--整合jwt實現token認證-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
  • 在 SpringBoot 專案配置 config 包下建立 ShiroConfig 配置類

@Configuration
public class ShiroConfig {

    /**
     * ShiroFilterFactoryBean
     * <p>
     * anon:無需認證就可以訪問
     * authc:必須認證才能訪問
     * user:必須擁有 記住我 功能才能用
     * perms:擁有對某個資源的許可權能訪問
     * role:擁有某個角色許可權能訪問
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 設定安全管理器
        factoryBean.setSecurityManager(defaultWebSecurityManager);
        // 新增shiro的內建過濾器
        Map<String, String> filterMap = new LinkedHashMap<>();
        // 放行不需要許可權認證的介面
        // 網站首頁
        filterMap.put("/", "anon");
        filterMap.put("/index", "anon");
        filterMap.put("/index.html", "anon");
        // 不驗證跳轉介面
        filterMap.put("/into/**", "anon");

        // 需要許可權認證的介面
        // 驗證跳轉介面
        filterMap.put("/verifyInto/**", "authc");
        
        factoryBean.setFilterChainDefinitionMap(filterMap);

        // 訪問沒有授權的資源
        factoryBean.setLoginUrl("redirect:/into/login");
        // 設定無許可權時跳轉的url
        factoryBean.setUnauthorizedUrl("redirect:/into/login");

        return factoryBean;
    }

    /**
     * 管理shiro的生命週期
     */
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 注入 密碼登入CustomRealm
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public UserPasswordRealm userPasswordRealm() {
        return new UserPasswordRealm();
    }

    /**
     * 注入 郵箱驗證登入EmailRealm
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public UserEmailRealm userEmailRealm() {
        return new UserEmailRealm();
    }

    /**
     * 預設安全管理器
     */
    @Bean
    public DefaultWebSecurityManager securityManager(UserPasswordRealm userPasswordRealm, UserEmailRealm userEmailRealm, AbstractAuthenticator abstractAuthenticator) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        List<Realm> realms = new ArrayList<>();
        realms.add(userPasswordRealm);
        realms.add(userEmailRealm);
        defaultWebSecurityManager.setRealms(realms);
        // 記住我
        defaultWebSecurityManager.setRememberMeManager(cookieRememberMeManager());
        defaultWebSecurityManager.setAuthenticator(abstractAuthenticator);
        return defaultWebSecurityManager;
    }

    /**
     * 認證器 把我們的自定義驗證加入到認證器中
     */
    @Bean
    public AbstractAuthenticator abstractAuthenticator(UserPasswordRealm userPasswordRealm, UserEmailRealm userEmailRealm) {
        // 自定義模組化認證器,用於解決多realm丟擲異常問題
        //開始沒用自定義異常問題,發現不管是賬號密碼錯誤還是什麼錯誤
        //shiro只會丟擲一個AuthenticationException異常
        ModularRealmAuthenticator authenticator = new MyCustomModularRealmAuthenticator();
        // 認證策略:AtLeastOneSuccessfulStrategy(預設),AllSuccessfulStrategy,FirstSuccessfulStrategy
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        // 加入realms
        List<Realm> realms = new ArrayList<>();
        realms.add(userPasswordRealm);
        realms.add(userEmailRealm);
        authenticator.setRealms(realms);
        return authenticator;
    }

    /**
     * 加入shiro註解  代理生成器 切面
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 加入shiro註解 切點
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 設定cookie 記住我生成cookie
     */
    @Bean
    public CookieRememberMeManager cookieRememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

    /**
     * 設定cookie有效時間
     */
    @Bean
    public SimpleCookie rememberMeCookie() {
        /*這個引數是cookie的名稱,對應前端頁面的checkbox的name=remremberMe*/
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        /*cookie的有效時間為30天,單位秒*/
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

}
  • 建立自定義驗證器 MyCustomModularRealmAuthenticator 類

public class MyCustomModularRealmAuthenticator extends ModularRealmAuthenticator {

    @Override
    protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
        AuthenticationStrategy authenticationStrategy = this.getAuthenticationStrategy();
        AuthenticationInfo authenticationInfo = authenticationStrategy.beforeAllAttempts(realms, token);

        Iterator var5 = realms.iterator();
        while (var5.hasNext()) {
            Realm realm = (Realm) var5.next();
            authenticationInfo = authenticationStrategy.beforeAttempt(realm, token, authenticationInfo);
            if (realm.supports(token)) {

                AuthenticationInfo info = null;
                Throwable t = null;

                info = realm.getAuthenticationInfo(token);

                authenticationInfo = authenticationStrategy.afterAttempt(realm, token, info, authenticationInfo, t);
            }
        }
        authenticationInfo = authenticationStrategy.afterAllAttempts(token, authenticationInfo);
        return authenticationInfo;
    }
}
  • 建立密碼登入時驗證授權 UserPasswordRealm 類

@Component
public class UserPasswordRealm extends AuthorizingRealm {

    // 注入使用者業務
    @Autowired
    private UserMapper userMapper;

    /**
     * 授權
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("————密碼授權————doGetAuthorizationInfo————");

        return null;
    }

    /**
     * 認證
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("————密碼認證————doGetAuthenticationInfo————");

        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        // 連線資料庫  查詢使用者資料
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("user_name", userToken.getUsername());
        User user = userMapper.selectOne(wrapper);
        // 驗證使用者
        if (user == null) {
            throw new UnknownAccountException();
        }
        return new SimpleAuthenticationInfo("", user.getUserPassword(), "");
    }

    /**
     * 用來判斷是否使用當前的 realm
     *
     * @param var1 傳入的token
     * @return true就使用,false就不使用
     */
    @Override
    public boolean supports(AuthenticationToken var1) {
        return var1 instanceof UsernamePasswordToken;
    }

}
  • 建立郵件驗證碼登入時驗證授權 UserEmailRealm 

@Component
public class UserEmailRealm extends AuthorizingRealm {

    // 注入使用者業務
    @Autowired
    UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("————郵箱登入授權————doGetAuthorizationInfo————");
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("————郵箱登入認證————doGetAuthenticationInfo————");
        UserEmailToken userEmailToken = (UserEmailToken) token;
        String userEmail = (String) userEmailToken.getPrincipal();
        // 連線資料庫  查詢使用者資料
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("user_email", userEmail);
        User user = userService.getOne(wrapper);
        //因為沒有密碼,並且驗證碼在之前就驗證了
        if (user == null) {
            throw new UnknownAccountException();
        }
        return new SimpleAuthenticationInfo("", userEmail, "");
    }

    /**
     * 用來判斷是否使用當前的 realm
     *
     * @param var1 傳入的token
     * @return true就使用,false就不使用
     */
    @Override
    public boolean supports(AuthenticationToken var1) {
        return var1 instanceof UserEmailToken;
    }
}
  • 建立郵件驗證碼登入驗證通過生成令牌的 UserEmailToken 類(密碼登入時使用shiro預設的 UsernamePasswordToken 令牌)

@Data  // 使用lombok 生成get方法、set方法
public class UserEmailToken implements HostAuthenticationToken, RememberMeAuthenticationToken {

    private String userEmail;
    private boolean rememberMe;
    private String host;

    public UserEmailToken() {
        this.rememberMe = false;
    }

    public UserEmailToken(String userEmail) {
        this(userEmail, false, null);
    }

    public UserEmailToken(String userEmail, boolean rememberMe) {
        this(userEmail, rememberMe, null);
    }

    public UserEmailToken(String userEmail, boolean rememberMe, String host) {
        this.userEmail = userEmail;
        this.rememberMe = rememberMe;
        this.host = host;
    }

    @Override
    public String getHost() {
        return host;
    }

    @Override
    public boolean isRememberMe() {
        return rememberMe;
    }

    /**
     * 重寫getPrincipal方法
     */
    @Override
    public Object getPrincipal() {
        return userEmail;
    }

    /**
     * 重寫getCredentials方法
     */
    @Override
    public Object getCredentials() {
        return userEmail;
    }
}
  • 建立密碼鹽值加密 MDPasswordUtil 工具類 

public class MDPasswordUtil {

    public String getMDPasswordUtil(String userName, String userPassword) {
        String hashAlgorithmName = "MD5";  // 加密方式:md5加密
        Object credentials = userPassword;  // 密碼
        Object salt = ByteSource.Util.bytes(userName); //
        int hashIterations = 512;  // 加密次數
        Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
        return result.toString();
    }
}
  • 控制層使用者密碼登入

// 使用者密碼登入
    @PostMapping("/passwordLogin")
    public String userLogin(@RequestParam("userName") String userName,
                            @RequestParam("userPassword") String userPassword,
                            HttpSession session, Model model) {
        // 獲取當前的使用者
        Subject subject = SecurityUtils.getSubject();
        // 對密碼進行MD5鹽值加密
        String md5Password = new MDPasswordUtil().getMDPasswordUtil(userName, userPassword);
        // 封裝使用者的登入資料
        UsernamePasswordToken token = new UsernamePasswordToken(userName, md5Password);
        //rememberme記住我
        token.setRememberMe(true);
        try {
            // 登入,驗證,儲存令牌
            subject.login(token);

            //查詢登入資訊
            QueryWrapper<User> wrapper = new QueryWrapper<>();
            wrapper.eq("user_name", userName);
            User user = userService.getOne(wrapper);
            //儲存登入使用者資訊
            session.setAttribute(user.getUserId().toString(), user);

           return "admin";
        } catch (UnknownAccountException e) {
            model.addAttribute("userError", "使用者名稱錯誤!請重新輸入。");
            return "login";
        } catch (IncorrectCredentialsException ice) {
            model.addAttribute("pwError", "密碼錯誤!請重新輸入。");
            return "login";
        }
    }
  • 控制層使用者郵件驗證碼密碼登入

 // 使用者郵箱登入
    @PostMapping("/emailLogin")
    public String emailLogin(@RequestParam("userEmail") String userEmail,
                             @RequestParam("emailCode") String emailCode,
                             HttpSession session, Model model) {
        // 根據userEmail從session中取出傳送的驗證碼
        String sendEmailCode = (String) session.getAttribute(userEmail);
        // 比對驗證碼
        if (StringUtils.isNoneBlank(sendEmailCode) && sendEmailCode.equals(emailCode)) {
            try {
                UserEmailToken token = new UserEmailToken(userEmail);
                //rememberme記住我
                token.setRememberMe(true);
                // 登入,驗證,儲存令牌
                Subject subject = SecurityUtils.getSubject();
                subject.login(token);

                //查詢登入資訊
                QueryWrapper<User> wrapper = new QueryWrapper<>();
                wrapper.eq("user_email", userEmail);
                User user = userService.getOne(wrapper);
                //儲存登入使用者資訊
                session.setAttribute(user.getUserId().toString(), user);

                // 銷燬驗證碼
                session.removeAttribute(emailCode);

                return "admin";
            } catch (Exception e) {
                model.addAttribute("error", "驗證碼錯誤!請重新輸入。");
                return "login";
            }
        } else {
            return "login";
        }
    }
  • SpringBoot 整合 Shiro 密碼登入與郵件驗證碼登入(多 Realm 認證)就可以了 (有點多,哈哈哈)

 

推薦大神:狂神說Java

 

相關文章