SpringBoot整合Shiro多realm登錄配置,另外附倆個教程

SpringBoot整合Shiro,通過用戶、角色、權限三者關聯實現權限管理

本篇文章主要介紹 Shiro 多 realm,根據不同的登錄類型指定不同的 realm。

所謂免密登錄,就是區別正常的密碼登錄。比如,我現在要實現第三方登錄,當驗證了是李四,現在要讓他通過 shiro 的 subject.login(),但是不知道他的密碼(密碼加密了),我們不能拿數據庫裏的密碼去登錄,除非重新寫 Realm。

所以需要多個 Realm,一個是密碼登錄(shiro會根據用戶的輸入的密碼和加密方法加密後比較);一個免密登錄(允許使用數據庫密碼登錄,shiro不進行任何加密)。

實現的過程簡單說下:

重寫 UsernamePasswordToken,加一個 loginType 屬性,subject.login() 的時候傳入 loginType; 重寫 ModularRealmAuthenticator 中的 doAuthenticate() 方法,根據傳進來的 loginType 來指定使用哪個 Realm。

一、Shiro 配置

1.兩個 Realm

NormalRealm.java 密碼登錄的 Realm

/**
 * 默認的realm
 *
 * @author lp
 */
@Slf4j
public class NormalRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermissionService permissionService;
    @Autowired
    private LocaleMessageUtil localeMessageUtil;
    /**
     * 認證信息(身份驗證) Authentication 是用來驗證用戶身份
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("認證-->MyShiroRealm.doGetAuthenticationInfo()");
        //1.驗證用戶名
        User user = null;
        String loginName = (String) token.getPrincipal();
        if (Validator.isEmail(loginName)) {
            user = userService.findByEmail(loginName);
        } else {
            user = userService.findByUserName(loginName);
        }
        if (user == null) {
            //用戶不存在
            log.info("用戶不存在! ", loginName, token.getCredentials());
            return null;
        }
        //2.首先判斷是否已經被禁用已經是否已經過了10分鐘
        Date loginLast = DateUtil.date();
        if (null != user.getLoginLast()) {
            loginLast = user.getLoginLast();
        }
        Long between = DateUtil.between(loginLast, DateUtil.date(), DateUnit.MINUTE);
        if (StringUtils.equals(user.getLoginEnable(), TrueFalseEnum.FALSE.getDesc()) && (between < CommonParamsEnum.TEN.getValue())) {
            log.info("賬號已鎖定!", loginName, token.getCredentials());
            throw new LockedAccountException(localeMessageUtil.getMessage("code.admin.login.disabled"));
        }
        userService.updateUserLoginLast(user, DateUtil.date());
        //3.封裝authenticationInfo,準備驗證密碼
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user, // 用戶名
                user.getUserPass(), // 密碼
                ByteSource.Util.bytes("sens"), // 鹽
                getName() // realm name
        );
        System.out.println("realName:" + getName());
        return authenticationInfo;
    }
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("授權-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principals.getPrimaryPrincipal();
        List<Role> roles = roleService.listRolesByUserId(user.getUserId());
        for (Role role : roles) {
            authorizationInfo.addRole(role.getRole());
            List<Permission> permissions = permissionService.listPermissionsByRoleId(role.getId());
            for (Permission p : permissions) {
                authorizationInfo.addStringPermission(p.getPermission());
            }
        }
        return authorizationInfo;
    }
}

FreeRealm.java 密碼不加密的 Realm

/**
 * 免密登錄,輸入的密碼和原密碼一致
 *
 * @author lp
 */
@Slf4j
public class FreeRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermissionService permissionService;
    @Autowired
    private LocaleMessageUtil localeMessageUtil;
    /**
     * 認證信息(身份驗證) Authentication 是用來驗證用戶身份
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.驗證用戶名
        User user = null;
        String loginName = (String) token.getPrincipal();
        if (Validator.isEmail(loginName)) {
            user = userService.findByEmail(loginName);
        } else {
            user = userService.findByUserName(loginName);
        }
        if (user == null) {
            //用戶不存在
            log.info("第三方登錄,用戶不存在! 登錄名:{}, 密碼:{}", loginName,token.getCredentials());
            return null;
        }
        //3.封裝authenticationInfo,準備驗證密碼
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user, // 用戶名
                user.getUserPass(), // 密碼
                null,
                getName() // realm name
        );
        return authenticationInfo;
    }
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("權限配置-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principals.getPrimaryPrincipal();
        List<Role> roles = roleService.listRolesByUserId(user.getUserId());
        for (Role role : roles) {
            authorizationInfo.addRole(role.getRole());
            List<Permission> permissions = permissionService.listPermissionsByRoleId(role.getId());
            for (Permission p : permissions) {
                authorizationInfo.addStringPermission(p.getPermission());
            }
        }
        return authorizationInfo;
    }
}

2.ShiroConfig

/**
 * @author lp
 */
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //攔截器.
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
        // 配置不會被攔截的鏈接 順序判斷
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/upload/**", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/admin/login", "anon");
        filterChainDefinitionMap.put("/admin/getLogin", "anon");
        //配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
        filterChainDefinitionMap.put("/logout", "logout");
        //<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最爲下邊 -->:這是一個坑呢,一不小心代碼就不好使了;
        //<!-- authc:所有url都必須認證通過纔可以訪問; anon:所有url都都可以匿名訪問-->
        filterChainDefinitionMap.put("/admin/**", "authc");
        filterChainDefinitionMap.put("/backup/**", "authc");
        filterChainDefinitionMap.put("/**", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        // 如果不設置默認會自動尋找Web工程根目錄下的"/login"頁面
        shiroFilterFactoryBean.setLoginUrl("/admin/login");
        // 登錄成功後要跳轉的鏈接
        shiroFilterFactoryBean.setSuccessUrl("/");
        //未授權界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        return shiroFilterFactoryBean;
    }
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setAuthenticator(modularRealmAuthenticator());
        List<Realm> realms = new ArrayList<>();
        //密碼登錄realm
        realms.add(normalRealm());
        //免密登錄realm
        realms.add(freeRealm());
        securityManager.setRealms(realms);
        return securityManager;
    }
    /**
     * 系統自帶的Realm管理,主要針對多realm
     * */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator(){
        //自己重寫的ModularRealmAuthenticator
        UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }
    /**
     * 需要密碼登錄的realm
     *
     * @return MyShiroRealm
     */
    @Bean
    public NormalRealm normalRealm() {
        NormalRealm normalRealm = new NormalRealm();
        normalRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return normalRealm;
    }
    /**
     * 免密登錄realm
     *
     * @return MyShiroRealm
     */
    @Bean
    public FreeRealm freeRealm() {
        FreeRealm realm = new FreeRealm();
        //不需要加密,直接用數據庫密碼進行登錄
        return realm;
    }
    /**
     * 憑證匹配器
     * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了
     *  所以我們需要修改下doGetAuthenticationInfo中的代碼;
     * )
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這裏使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(10);//散列的次數,md5("")
        return hashedCredentialsMatcher;
    }
    /**
     *  開啓shiro aop註解支持.
     *  使用代理方式;所以需要開啓代碼支持;
     * @param securityManager
     * @return
     */
    /** * Shiro生命週期處理器 * @return */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
}

多 Realm 模式需要重寫 ModularRealmAuthenticator

3.重寫ModularRealmAuthenticator

UserModularRealmAuthenticator.java

/**
 * @author lp
 */
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException {
        System.out.println("UserModularRealmAuthenticator:method doAuthenticate() execute ");
        // 判斷getRealms()是否返回爲空
        assertRealmsConfigured();
        // 強制轉換回自定義的CustomizedToken
        UserToken userToken = (UserToken) authenticationToken;
        // 登錄類型
        String loginType = userToken.getLoginType();
        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 登錄類型對應的所有Realm
        List<Realm> typeRealms = new ArrayList<>();
        for (Realm realm : realms) {
            if (realm.getName().contains(loginType)) {
                typeRealms.add(realm);
            }
        }
        // 判斷是單Realm還是多Realm
        if (typeRealms.size() == 1){
            System.out.println("doSingleRealmAuthentication() execute ");
            return doSingleRealmAuthentication(typeRealms.get(0), userToken);
        }
        else{
            System.out.println("doMultiRealmAuthentication() execute ");
            return doMultiRealmAuthentication(typeRealms, userToken);
        }
    }
}

4.枚舉類 LoginType

LoginType.java

/**
 * @author lp
 */
public enum LoginType {
    /**
     * 密碼登錄
     */
    NORMAL("Normal"),
    /**
     * 免密碼登錄
     */
    FREE("Free");
    private String desc;
    LoginType(String desc) {
        this.desc = desc;
    }
    public String getDesc() {
        return desc;
    }
}

5.自定義UsernamePasswordToken

UserToken.java

/**
 *
 * 自定義UsernamePasswordToken
 * 必須傳loginType
 *
 * @author lp
 */
@Data
public class UserToken extends UsernamePasswordToken {
    private String loginType;
    public UserToken() {
    }
    public UserToken(final String username, final String password,
                     final String loginType) {
        super(username, password);
        this.loginType = loginType;
    }
}

二、登錄
1.正常的密碼登錄

@PostMapping(value = "/getLogin")
@ResponseBody
public JsonResult getLogin(@ModelAttribute("loginName") String loginName, @ModelAttribute("loginPwd") String loginPwd) {
        Subject subject = SecurityUtils.getSubject();
        UserToken token = new UserToken(loginName, loginPwd, LoginType.FREE.getDesc());
        try {
            subject.login(token);
            if (subject.isAuthenticated()) {
                //登錄成功,修改登錄錯誤次數爲0
                User user = (User) subject.getPrincipal();
                userService.updateUserLoginNormal(user);
                return new JsonResult(ResultCodeEnum.SUCCESS.getCode(),"登錄成功");
            }
        } catch (UnknownAccountException e) {
            ...
        }
       ...
    }

2.第三方登錄,授權成功後,免密登錄

/**
 * @author lp
 */
@Controller
@Slf4j
public class AuthController {
    @Autowired
    private QQAuthService qqAuthService;
    @Autowired
    private UserService userService;
    @Autowired
    private ThirdAppBindService thirdAppBindService;
    @Autowired
    private LogService logService;
    /**
     * 第三方授權後會回調此方法,並將code傳過來
     *
     * @param code code
     * @return
     */
    @GetMapping("/oauth/qq/callback")
    public String oauthByQQ(@RequestParam(value = "code") String code, HttpServletRequest request) {
        Response<String> tokenResponse = qqAuthService.getAccessToken(code);
        if (tokenResponse.isSuccess()) {
            Response<String> openidResponse = qqAuthService.getOpenId(tokenResponse.getData());
            if (openidResponse.isSuccess()) {
                //根據openId去找關聯的用戶
                ThirdAppBind bind = thirdAppBindService.findByAppTypeAndOpenId(BindTypeEnum.QQ.getValue(), openidResponse.getData());
                if (bind != null && bind.getUserId() != null) {
                    //執行Login操作
                    User user = userService.findByUserId(bind.getUserId());
                    if (user != null) {
                        Subject subject = SecurityUtils.getSubject();
                        UserToken userToken = new UserToken(user.getUserName(), user.getUserPass(), LoginType.FREE.getDesc());  
                        try {
                            subject.login(userToken);
                        } catch (Exception e) {
                            e.printStackTrace();
                            log.error("第三方登錄(QQ)免密碼登錄失敗, cause:{}", e);
                            return "redirect:/admin/login";
                        }
                        logService.saveByLog(new Log(LogsRecord.LOGIN, LogsRecord.LOGIN_SUCCESS + "(QQ登錄)", ServletUtil.getClientIP(request), DateUtil.date()));
                        log.info("用戶[{}]登錄成功(QQ登錄)。", user.getUserDisplayName());
                        return "redirect:/admin";
                    }
                }
            }
        }
        return "redirect:/admin/login";
    }
}

對於springboot不瞭解的小夥伴提供倆個教程

Java微服務SpringBoot開發新聞資訊項目實戰課程

零基礎入門SpringBoot到高級實戰課程

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