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不瞭解的小夥伴提供倆個教程