javascript
SpringBoot+Shiro实现登陆拦截功能
????? 上一章講到使用自定義的方式來實(shí)現(xiàn)用戶登錄的功能,這章采用shiro來實(shí)現(xiàn)用戶登陸攔截的功能。
????? 首先介紹下Shiro:Apache Shiro是一個(gè)強(qiáng)大且易用的Java安全框架,執(zhí)行身份驗(yàn)證、授權(quán)、密碼學(xué)和會(huì)話管理,以下是shiro的整體的框架:
Subject: 即"用戶",外部應(yīng)用都是和Subject進(jìn)行交互的,subject記錄了當(dāng)前操作用戶,將用戶的概念理解為當(dāng)前操作的主體,可能是一個(gè)通過瀏覽器請(qǐng)求的用戶,也可能是一個(gè)運(yùn)行的程序。 Subject在shiro中是一個(gè)接口,接口中定義了很多認(rèn)證授相關(guān)的方法,外部程序通過subject進(jìn)行認(rèn)證授,而subject是通過SecurityManager安全管理器進(jìn)行認(rèn)證授權(quán)(Subject相當(dāng)于SecurityManager的門面)。
SecurityManager: 即安全管理器,它是shiro的核心,負(fù)責(zé)對(duì)所有的subject進(jìn)行安全管理。通過SecurityManager可以完成subject的認(rèn)證、授權(quán)等,實(shí)質(zhì)上SecurityManager是通過Authenticator進(jìn)行認(rèn)證,通過Authorizer進(jìn)行授權(quán),通過SessionManager進(jìn)行會(huì)話管理等。此外SecurityManager是一個(gè)接口,繼承了Authenticator, Authorizer, SessionManager這三個(gè)接口
Authenticator:是一個(gè)執(zhí)行對(duì)用戶的身份驗(yàn)證(登錄)的組件。通過與一個(gè)或多個(gè)Realm 協(xié)調(diào)來存儲(chǔ)相關(guān)的用戶/帳戶信息。從Realm中找到對(duì)應(yīng)的數(shù)據(jù),明確是哪一個(gè)登陸人。如果存在多個(gè)realm,則接口AuthenticationStrategy(策略)會(huì)確定什么樣算是登錄成功(例如,如果一個(gè)Realm成功,而其他的均失敗,是否登錄成功?)。它是一個(gè)接口,shiro提供ModularRealmAuthenticator實(shí)現(xiàn)類,通過ModularRealmAuthenticator基本上可以滿足大多數(shù)需求,也可以自定義認(rèn)證器。
Authorizer:即授權(quán)器,用戶通過認(rèn)證器認(rèn)證通過,在訪問功能時(shí)需要通過授權(quán)器判斷用戶是否有此功能的操作權(quán)限。就是用來判斷是否有權(quán)限,授權(quán),本質(zhì)就是訪問控制,控制哪些URL可以訪問.
Realm:即領(lǐng)域,相當(dāng)于datasource數(shù)據(jù)源,securityManager進(jìn)行安全認(rèn)證需要通過Realm獲取用戶權(quán)限數(shù)據(jù),通常一個(gè)數(shù)據(jù)源配置一個(gè)realm.s比如:如果用戶身份數(shù)據(jù)在數(shù)據(jù)庫那么realm就需要從數(shù)據(jù)庫獲取用戶身份信息。
注意:不要把realm理解成只是從數(shù)據(jù)源取數(shù)據(jù),在realm中還有認(rèn)證授權(quán)校驗(yàn)的相關(guān)的代碼。
SessionDAO:即會(huì)話dao,是對(duì)session會(huì)話操作的一套接口,SessionDao代替sessionManager來代替對(duì)session進(jìn)行增刪改查,允許用戶使用任何類型的數(shù)據(jù)源來存儲(chǔ)session數(shù)據(jù),也可以將數(shù)據(jù)引入到session框架來。比如要將session存儲(chǔ)到數(shù)據(jù)庫,可以通過jdbc將會(huì)話存儲(chǔ)到數(shù)據(jù)庫。
CacheManager:即緩存管理,用于管理其他shiro組件中維護(hù)和創(chuàng)建的cache實(shí)例,維護(hù)這些cache實(shí)例的生命周期,緩存那些從后臺(tái)獲取的用于用戶權(quán)限,驗(yàn)證的數(shù)據(jù),將它們存儲(chǔ)在緩存,這樣可以提高性能。順序:先從緩存中查找,再從后臺(tái)其他接口從其它數(shù)據(jù)源中進(jìn)行查找,可以用其他現(xiàn)代的企業(yè)級(jí)數(shù)據(jù)源來代替默認(rèn)的數(shù)據(jù)源來提高性能
Cryptography:即密碼管理,shiro提供了一套加密/解密的組件,方便開發(fā)。比如提供常用的散列、加/解密等功能。
我們可以把和shiro的交互用下圖來表示:
這個(gè)是Shiro身份認(rèn)證的流程圖:
(注:這個(gè)圖片是從其他博客拷貝過來的,)
這是Shiro的認(rèn)證流程:
流程如下:
1、首先調(diào)用Subject.isPermitted*/hasRole*接口,其會(huì)委托給SecurityManager,而SecurityManager接著會(huì)委托給Authorizer;
2、Authorizer是真正的授權(quán)者,如果我們調(diào)用如isPermitted(“user:view”),其首先會(huì)通過PermissionResolver把字符串轉(zhuǎn)換成相應(yīng)的Permission實(shí)例;
3、在進(jìn)行授權(quán)之前,其會(huì)調(diào)用相應(yīng)的Realm獲取Subject相應(yīng)的角色/權(quán)限用于匹配傳入的角色/權(quán)限;
4、Authorizer會(huì)判斷Realm的角色/權(quán)限是否和傳入的匹配,如果有多個(gè)Realm,會(huì)委托給ModularRealmAuthorizer進(jìn)行循環(huán)判斷,如果匹配如isPermitted*/hasRole*會(huì)返回true,否則返回false表示授權(quán)失敗。
?
ModularRealmAuthorizer進(jìn)行多Realm匹配流程:
1、首先檢查相應(yīng)的Realm是否實(shí)現(xiàn)了實(shí)現(xiàn)了Authorizer;
2、如果實(shí)現(xiàn)了Authorizer,那么接著調(diào)用其相應(yīng)的isPermitted*/hasRole*接口進(jìn)行匹配;
3、如果有一個(gè)Realm匹配那么將返回true,否則返回false。
?
如果Realm進(jìn)行授權(quán)的話,應(yīng)該繼承AuthorizingRealm,其流程是:
1.1、如果調(diào)用hasRole*,則直接獲取AuthorizationInfo.getRoles()與傳入的角色比較即可;
1.2、如果調(diào)用如isPermitted(“user:view”),首先通過PermissionResolver將權(quán)限字符串轉(zhuǎn)換成相應(yīng)的Permission實(shí)例,默認(rèn)使用WildcardPermissionResolver,即轉(zhuǎn)換為通配符的WildcardPermission;
2、通過AuthorizationInfo.getObjectPermissions()得到Permission實(shí)例集合;通過AuthorizationInfo.?getStringPermissions()得到字符串集合并通過PermissionResolver解析為Permission實(shí)例;然后獲取用戶的角色,并通過RolePermissionResolver解析角色對(duì)應(yīng)的權(quán)限集合(默認(rèn)沒有實(shí)現(xiàn),可以自己提供);
3、接著調(diào)用Permission. implies(Permission p)逐個(gè)與傳入的權(quán)限比較,如果有匹配的則返回true,否則false
現(xiàn)在開始上代碼:
Pom.xml
<!-- 支持JSP,必須導(dǎo)入這兩個(gè)依賴 --><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId><scope>provided</scope></dependency><dependency> <groupId>javax.servlet.jsp.jstl</groupId> <artifactId>jstl-api</artifactId> <version>1.2</version> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.4.0</version></dependency><dependency><groupId>postgresql</groupId><artifactId>postgresql</artifactId><version>8.4-702.jdbc4</version></dependency><dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.0</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.0.20</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.4</version></dependency>這邊還用了Mybatis的內(nèi)容,需要讀者自行去學(xué)習(xí)相關(guān)的知識(shí),這里不詳細(xì)介紹了。
項(xiàng)目的整體預(yù)覽:
login.jsp:這邊是一個(gè)簡(jiǎn)單的form表單
<form action="/loginUser" method="post"><input type="text" name="username"> <br><input type="password" name="password"> <br><input type="submit" value="提交"> </form>index.jsp:簡(jiǎn)單的展示界面
<h1> 歡迎登錄, ${user.username} </h1>Unauthorized.jsp:自定義跳轉(zhuǎn)的無權(quán)限界面
<body> Unauthorized! </body>appliaction.yml:
server: port: 8081session-timeout: 30tomcat.max-threads: 0tomcat.uri-encoding: UTF-8spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: org.postgresql.Driverurl: jdbc:postgresql://服務(wù)器地址:5432/庫名username: XXXXXpassword: XXXXXmvc:view:prefix: /pages/suffix: .jsp mybatis:mapper-locations: mappers/*.xmltype-aliases-pacakage: com.Pojo #映射的類型在Pojo下面這是存放的相對(duì)位置
TestController:控制器類
@Controller public class TestController {@RequestMapping("/login")public String login() {return "login";}@RequestMapping("/index")public String index() {return "index";}@RequestMapping("/logout")public String logout() {Subject subject = SecurityUtils.getSubject();//取出當(dāng)前驗(yàn)證主體if (subject != null) {subject.logout();//不為空,執(zhí)行一次logout的操作,將session全部清空}return "login";}@RequestMapping("unauthorized")public String unauthorized() {return "unauthorized";}@RequestMapping("/admin")@ResponseBody//注解之后只是返回json數(shù)據(jù),不返回界面public String admin() {return "admin success";}@RequestMapping("/edit")@ResponseBodypublic String edit() {return "edit success";}/** 整個(gè)form表單的驗(yàn)證流程:* * 將登陸的用戶/密碼傳入U(xiǎn)sernamePasswordToken,當(dāng)調(diào)用subject.login(token)開始,調(diào)用Relam的doGetAuthenticationInfo方法,開始密碼驗(yàn)證* 此時(shí)這個(gè)時(shí)候執(zhí)行我們自己編寫的CredentialMatcher(密碼匹配器),執(zhí)行doCredentialsMatch方法,具體的密碼比較實(shí)現(xiàn)在這實(shí)現(xiàn)* * */@RequestMapping("/loginUser")public String loginUser(@RequestParam("username") String username,@RequestParam("password") String password,HttpSession session) {UsernamePasswordToken token = new UsernamePasswordToken(username, password);Subject subject = SecurityUtils.getSubject();try {System.out.println("獲取到信息,開始驗(yàn)證!!");subject.login(token);//登陸成功的話,放到session中User user = (User) subject.getPrincipal();session.setAttribute("user", user);return "index";} catch (Exception e) {return "login";}} }ShiroConfiguration.java:自定義了Shiro的配置器
package com.Auth;import org.apache.shiro.cache.MemoryConstrainedCacheManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap;@Configuration public class ShiroConfiguration {//@Qualifier代表spring里面的@Bean("shiroFilter")public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();bean.setSecurityManager(manager);bean.setLoginUrl("/login");//提供登錄到urlbean.setSuccessUrl("/index");//提供登陸成功的urlbean.setUnauthorizedUrl("/unauthorized");/** 可以看DefaultFilter,這是一個(gè)枚舉類,定義了很多的攔截器authc,anon等分別有對(duì)應(yīng)的攔截器* */LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();filterChainDefinitionMap.put("/index", "authc");//代表著前面的url路徑,用后面指定的攔截器進(jìn)行攔截filterChainDefinitionMap.put("/login", "anon");filterChainDefinitionMap.put("/loginUser", "anon");filterChainDefinitionMap.put("/admin", "roles[admin]");//admin的url,要用角色是admin的才可以登錄,對(duì)應(yīng)的攔截器是RolesAuthorizationFilterfilterChainDefinitionMap.put("/edit", "perms[edit]");//擁有edit權(quán)限的用戶才有資格去訪問filterChainDefinitionMap.put("/druid/**", "anon");//所有的druid請(qǐng)求,不需要攔截,anon對(duì)應(yīng)的攔截器不會(huì)進(jìn)行攔截filterChainDefinitionMap.put("/**", "user");//所有的路徑都攔截,被UserFilter攔截,這里會(huì)判斷用戶有沒有登陸bean.setFilterChainDefinitionMap(filterChainDefinitionMap);//設(shè)置一個(gè)攔截器鏈return bean;}/** 注入一個(gè)securityManager* 原本以前我們是可以通過ini配置文件完成的,代碼如下:* 1、獲取SecurityManager工廠,此處使用Ini配置文件初始化SecurityManagerFactory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");2、得到SecurityManager實(shí)例 并綁定給SecurityUtilsSecurityManager securityManager = factory.getInstance();SecurityUtils.setSecurityManager(securityManager);* */@Bean("securityManager")public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {//這個(gè)DefaultWebSecurityManager構(gòu)造函數(shù),會(huì)對(duì)Subject,realm等進(jìn)行基本的參數(shù)注入DefaultWebSecurityManager manager = new DefaultWebSecurityManager();manager.setRealm(authRealm);//往SecurityManager中注入Realm,代替原本的默認(rèn)配置return manager;}//自定義的Realm@Bean("authRealm")public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {AuthRealm authRealm = new AuthRealm();//這邊可以選擇是否將認(rèn)證的緩存到內(nèi)存中,現(xiàn)在有了這句代碼就將認(rèn)證信息緩存的內(nèi)存中了authRealm.setCacheManager(new MemoryConstrainedCacheManager());//最簡(jiǎn)單的情況就是明文直接匹配,然后就是加密匹配,這里的匹配工作則就是交給CredentialsMatcher來完成authRealm.setCredentialsMatcher(matcher);return authRealm;}/* * Realm在驗(yàn)證用戶身份的時(shí)候,要進(jìn)行密碼匹配* 最簡(jiǎn)單的情況就是明文直接匹配,然后就是加密匹配,這里的匹配工作則就是交給CredentialsMatcher來完成* 支持任意數(shù)量的方案,包括純文本比較、散列比較和其他方法。除非該方法重寫,否則默認(rèn)值為* */@Bean("credentialMatcher")public CredentialMatcher credentialMatcher() {return new CredentialMatcher();}/** 以下AuthorizationAttributeSourceAdvisor,DefaultAdvisorAutoProxyCreator兩個(gè)類是為了支持shiro注解* */@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}@Beanpublic DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();creator.setProxyTargetClass(true);return creator;} }這里自定義了AuthRealm,CredentialsMatcher,來看看它們具體的代碼:
public class AuthRealm extends AuthorizingRealm{ //AuthenticatingRealm是抽象類,用于認(rèn)證@Autowiredprivate UserService userService;/** 真實(shí)授權(quán)抽象方法,供子類調(diào)用* * 這個(gè)是當(dāng)?shù)顷懗晒χ髸?huì)被調(diào)用,看當(dāng)前的登陸角色是有有權(quán)限來進(jìn)行操作* */@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {System.out.println("doGetAuthorizationInfo方法");User user = (User) principals.fromRealm(this.getClass().getName()).iterator().next();List<String> permissionList = new ArrayList<>();List<String> roleNameList = new ArrayList<>();Set<Role> roleSet = user.getRoles();//拿到角色if (CollectionUtils.isNotEmpty(roleSet)) {for(Role role : roleSet) {roleNameList.add(role.getRname());//拿到角色Set<Permission> permissionSet = role.getPermissions();if (CollectionUtils.isNotEmpty(permissionSet)) {for (Permission permission : permissionSet) {permissionList.add(permission.getName());}}}}SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.addStringPermissions(permissionList);//拿到權(quán)限info.addRoles(roleNameList);//拿到角色return info;}/** 用于認(rèn)證登錄,認(rèn)證接口實(shí)現(xiàn)方法,該方法的回調(diào)一般是通過subject.login(token)方法來實(shí)現(xiàn)的* AuthenticationToken 用于收集用戶提交的身份(如用戶名)及憑據(jù)(如密碼):* AuthenticationInfo是包含了用戶根據(jù)username返回的數(shù)據(jù)信息,用于在匹馬比較的時(shí)候進(jìn)行相互比較* * shiro的核心是java servlet規(guī)范中的filter,通過配置攔截器,使用攔截器鏈來攔截請(qǐng)求,如果允許訪問,則通過。* 通常情況下,系統(tǒng)的登錄、退出會(huì)配置攔截器。登錄的時(shí)候,調(diào)用subject.login(token),token是用戶驗(yàn)證信息,* 這個(gè)時(shí)候會(huì)在Realm中doGetAuthenticationInfo方法中進(jìn)行認(rèn)證。這個(gè)時(shí)候會(huì)把用戶提交的驗(yàn)證信息與數(shù)據(jù)庫中存儲(chǔ)的認(rèn)證信息,將所有的數(shù)據(jù)拿到,在匹配器中進(jìn)行比較* 這邊是我們自己實(shí)現(xiàn)的CredentialMatcher類的doCredentialsMatch方法,返回true則一致,false則登陸失敗* 退出的時(shí)候,調(diào)用subject.logout(),會(huì)清除回話信息* * */@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {System.out.println("將用戶,密碼填充完UsernamePasswordToken之后,進(jìn)行subject.login(token)之后");UsernamePasswordToken userpasswordToken = (UsernamePasswordToken) token;//這邊是界面的登陸數(shù)據(jù),將數(shù)據(jù)封裝成tokenString username = userpasswordToken.getUsername();User user = userService.findByUsername(username);return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName());}} /** 密碼校驗(yàn)方法繼承SimpleCredentialsMatcher或HashedCredentialsMatcher類,自定義實(shí)現(xiàn)doCredentialsMatch方法* */ public class CredentialMatcher extends SimpleCredentialsMatcher {/** 這里是進(jìn)行密碼匹配的方法,自己定義* 通過用戶的唯一標(biāo)識(shí)得到 AuthenticationInfo 然后和 AuthenticationToken (用戶名 密碼),進(jìn)行比較* */@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {System.out.println("這邊是密碼校對(duì)");UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;String password = new String(usernamePasswordToken.getPassword());String dbPassword = (String) info.getCredentials();//數(shù)據(jù)庫里的密碼return this.equals(password, dbPassword);} }UserMapper.java:
public interface UserMapper {User findByUsername(@Param("username") String username); }UserMapper對(duì)應(yīng)的UserMapper.xml如下:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.Mapper.UserMapper"><resultMap id="userMap" type="com.Pojo.User"><id property="uid" column="uid" /><id property="username" column="username" /><id property="password" column="password" /><collection property = "roles" ofType="com.Pojo.Role"><id property="rid" column="rid" /><id property="rname" column="rname" /><collection property="permissions" ofType="com.Pojo.Permission"><id property="pid" column="pid" /><id property="name" column="name" /><id property="url" column="url" /></collection></collection></resultMap><select id="findByUsername" parameterType="string" resultMap="userMap">SELECT u.*, r.*, p.*FROM "user" uINNER JOIN user_role ur on ur.uid = u.uidINNER JOIN role r on r.rid = ur.ridINNER JOIN permission_role pr on pr.rid = r.ridINNER JOIN permission p on pr.pid = p.pidWHERE u.username = #{username}</select> </mapper>這邊注意,在我springBoot的啟動(dòng)類中,已經(jīng)把包掃描了@MapperScan("com.Mapper")
@SpringBootApplication
@MapperScan("com.Mapper")
public class SpringBootShiroApplication {
?? ?public static void main(String[] args) {
?? ??? ?SpringApplication.run(SpringBootShiroApplication.class, args);
?? ?}
}
這邊還有service類,和service的實(shí)現(xiàn)類:
public interface UserService {
?? User findByUsername(String username);
}
@Service
public class UserServiceImpl implements UserService{
?? ?@Resource
?? ?private UserMapper userMapper;
?? ?@Override
?? ?public User findByUsername(String username) {
?? ??? ?return userMapper.findByUsername(username);
?? ?}
}
另外這邊也定義了幾個(gè)Pojo:
User.java:用戶類
public class User {private Integer uid;private String username;private String password;private Set<Role> roles = new HashSet<Role>();public Integer getUid() {return uid;}public void setUid(Integer uid) {this.uid = uid;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public Set<Role> getRoles() {return roles;}public void setRoles(Set<Role> roles) {this.roles = roles;} }Permission.java:權(quán)限類
public class Permission {private Integer pid;private String name;private String url;public Integer getPid() {return pid;}public void setPid(Integer pid) {this.pid = pid;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}}Role.java:角色類
public class Role {private Integer rid;private String rname;private Set<Permission> permissions = new HashSet<>();//一個(gè)角色有多個(gè)權(quán)限private Set<User> users = new HashSet<>();public Integer getRid() {return rid;}public void setRid(Integer rid) {this.rid = rid;}public String getRname() {return rname;}public void setRname(String rname) {this.rname = rname;}public Set<Permission> getPermissions() {return permissions;}public void setPermissions(Set<Permission> permissions) {this.permissions = permissions;}public Set<User> getUsers() {return users;}public void setUsers(Set<User> users) {this.users = users;} }具體的sql如下:
-------權(quán)限表------ CREATE TABLE permission (pid serial NOT NULL,name character varying(255) NOT NULL,url character varying(255),CONSTRAINT permission_pkey PRIMARY KEY (pid) ) WITH (OIDS=FALSE ); ALTER TABLE permissionOWNER TO logistics;INSERT INTO permission values('1','add','') INSERT INTO permission values('2','delete','') INSERT INTO permission values('3','edit','') INSERT INTO permission values('4','query','')-------用戶表------ CREATE TABLE "user" (uid serial NOT NULL,username character varying(255) NOT NULL,password character varying(255),CONSTRAINT user_pkey PRIMARY KEY (uid) ) WITH (OIDS=FALSE ); ALTER TABLE "user"OWNER TO logistics;INSERT INTO "user" values('1','admin','123456') INSERT INTO "user" values('2','demo','123456')-------角色表------ CREATE TABLE role (rid serial NOT NULL,rname character varying(255) NOT NULL,CONSTRAINT role_pkey PRIMARY KEY (rid) ) WITH (OIDS=FALSE ); ALTER TABLE roleOWNER TO logistics;INSERT INTO role values('1','admin') INSERT INTO role values('2','customer')-----權(quán)限角色關(guān)系表----- CREATE TABLE permission_role (rid integer NOT NULL,pid integer NOT NULL,CONSTRAINT permission_role_pkey PRIMARY KEY (pid,rid) ) WITH (OIDS=FALSE ); ALTER TABLE permission_roleOWNER TO logistics;INSERT INTO permission_role values(1,1) INSERT INTO permission_role values(1,2) INSERT INTO permission_role values(1,3) INSERT INTO permission_role values(1,4) INSERT INTO permission_role values(2,1) INSERT INTO permission_role values(2,4)-----用戶角色關(guān)系表----- CREATE TABLE user_role (rid integer NOT NULL,uid integer NOT NULL,CONSTRAINT user_role_pkey PRIMARY KEY (uid, rid) ) WITH (OIDS=FALSE ); ALTER TABLE user_roleOWNER TO logistics;INSERT INTO user_role values(1,1) INSERT INTO user_role values(2,2)此時(shí)我們開始測(cè)試:
輸入localhost:8081/admin,由于我們?cè)赟hiroConfiguration中配置了一個(gè)攔截器鏈,對(duì)應(yīng)的URL路徑都會(huì)被對(duì)應(yīng)的攔截器給攔截來處理。
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();filterChainDefinitionMap.put("/index", "authc");//代表著前面的url路徑,用后面指定的攔截器進(jìn)行攔截filterChainDefinitionMap.put("/login", "anon");filterChainDefinitionMap.put("/loginUser", "anon");filterChainDefinitionMap.put("/admin", "roles[admin]");//admin的url,要用角色是admin的才可以登錄,對(duì)應(yīng)的攔截器是RolesAuthorizationFilterfilterChainDefinitionMap.put("/edit", "perms[edit]");//擁有edit權(quán)限的用戶才有資格去訪問filterChainDefinitionMap.put("/druid/**", "anon");//所有的druid請(qǐng)求,不需要攔截,anon對(duì)應(yīng)的攔截器不會(huì)進(jìn)行攔截filterChainDefinitionMap.put("/**", "user");//所有的路徑都攔截,被UserFilter攔截,這里會(huì)判斷用戶有沒有登陸bean.setFilterChainDefinitionMap(filterChainDefinitionMap);//設(shè)置一個(gè)攔截器鏈這里我們可以看到,admin路徑是被roles對(duì)應(yīng)的攔截器RolesAuthorizationFilter攔截,在方法isAccessAllowed中進(jìn)行處理,判斷是不是admin角色的用戶,是這個(gè)角色的才可以訪問,否則前往自己定義的無權(quán)限界面,這里別名對(duì)應(yīng)的攔截器是在DefaultFilter這個(gè)枚舉類中有定義:
anon(AnonymousFilter.class),authc(FormAuthenticationFilter.class),authcBasic(BasicHttpAuthenticationFilter.class),logout(LogoutFilter.class),noSessionCreation(NoSessionCreationFilter.class),perms(PermissionsAuthorizationFilter.class),port(PortFilter.class),rest(HttpMethodPermissionFilter.class),roles(RolesAuthorizationFilter.class),ssl(SslFilter.class),user(UserFilter.class);由于現(xiàn)在沒有登錄,所以一開始會(huì)前往登錄界面,填寫用戶賬號(hào)和密碼,點(diǎn)擊提交,因?yàn)槲覀僨orm表單的action是loginUser,此時(shí)數(shù)據(jù)提交到Controller中對(duì)應(yīng)的處理方法中:
/** 整個(gè)form表單的驗(yàn)證流程:* * 將登陸的用戶/密碼傳入U(xiǎn)sernamePasswordToken,當(dāng)調(diào)用subject.login(token)開始,調(diào)用Relam的doGetAuthenticationInfo方法,開始密碼驗(yàn)證* 此時(shí)這個(gè)時(shí)候執(zhí)行我們自己編寫的CredentialMatcher(密碼匹配器),執(zhí)行doCredentialsMatch方法,具體的密碼比較實(shí)現(xiàn)在這實(shí)現(xiàn)* * */@RequestMapping("/loginUser")public String loginUser(@RequestParam("username") String username,@RequestParam("password") String password,HttpSession session) {UsernamePasswordToken token = new UsernamePasswordToken(username, password);Subject subject = SecurityUtils.getSubject();try {System.out.println("獲取到信息,開始驗(yàn)證!!");subject.login(token);//登陸成功的話,放到session中User user = (User) subject.getPrincipal();session.setAttribute("user", user);return "index";} catch (Exception e) {return "login";}}我們會(huì)把用戶名,密碼存入到UsernamePasswordToken中,UsernamePasswordToken是一個(gè)用戶,密碼認(rèn)證令牌,里面有用戶名,密碼,是否緩存等屬性。然后代碼就會(huì)跳轉(zhuǎn)到我們自己編寫的Realm--AuthRealm的doGetAuthenticationInfo方法(具體可以看這篇博文https://www.cnblogs.com/ccfdod/p/6436353.html 這理由詳細(xì)的介紹,這個(gè)代碼調(diào)用如下:subject.login(token)-->DelegatingSubject類的login方法-->SecurityManager的login-->DefaultSecurityManager的login方法-->AuthenticatingSecurityManager的authenticate方法-->實(shí)現(xiàn)類AuthenticatingRealm中的getAuthenticationInfo方法)。在我們自己的getAuthenticationInfo方法中,我們根據(jù)用戶名查詢出用戶的信息,返回AuthenticationInfo對(duì)象,如果token與獲取到的AuthenticationInfo都不為空,緩存AuthenticationInfo信息。接著代碼會(huì)跳轉(zhuǎn)到我們的憑證驗(yàn)證的方法CredentialMatcher類的doCredentialsMatch方法:
@Overridepublic boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {System.out.println("這邊是密碼校對(duì)");UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;String password = new String(usernamePasswordToken.getPassword());String dbPassword = (String) info.getCredentials();//數(shù)據(jù)庫里的密碼return this.equals(password, dbPassword);}其實(shí)我們?cè)谡{(diào)用AuthenticatingRealm的getAuthenticationInfo方法時(shí):
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { //otherwise not cached, perform the lookup: info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }當(dāng)AuthenticationInfo查出來不為空時(shí),進(jìn)行憑證密碼匹配,調(diào)用assertCredentialsMatch(token,info):
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { CredentialsMatcher cm = getCredentialsMatcher(); if (cm != null) { if (!cm.doCredentialsMatch(token, info)) { //not successful - throw an exception to indicate this: String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; throw new IncorrectCredentialsException(msg); } } else { throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " + "credentials during authentication. If you do not wish for credentials to be examined, you " + "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); } }這邊調(diào)用cm.doCredentialsMatch(token, info)方法,這邊要闡述下CredentialsMatcher是一個(gè)接口,用來憑證密碼匹配的,繼承并實(shí)現(xiàn)doCredentialsMatch方法即可,這邊我們自定義的CredentialMatcher類,繼承了SimpleCredentialsMatcher類,而SimpleCredentialsMatcher實(shí)現(xiàn)了CredentialsMatcher方法。所以繼續(xù)接著上面思路的進(jìn)入我們的密碼匹配方法,如果匹配正確則返回true,如果驗(yàn)證失敗則返回false。此時(shí)一個(gè)完整的登錄驗(yàn)證完成。
那么當(dāng)我們繼續(xù)訪問其他的URL時(shí),會(huì)進(jìn)入我們授權(quán)的方法,AuthRealm類的doGetAuthorizationInfo(),主要是拿到登錄用戶的角色和權(quán)限,以此判斷該用戶是否有權(quán)限進(jìn)入U(xiǎn)RL,沒有權(quán)限則被跳轉(zhuǎn)到unauthorized.jsp界面,(?bean.setUnauthorizedUrl("/unauthorized");--原先設(shè)定的沒有訪問權(quán)限的情況)。
? ?這篇文章就講到這,如果讀者有補(bǔ)充,或者頁面中有不對(duì)的地方請(qǐng)指正。
總結(jié)
以上是生活随笔為你收集整理的SpringBoot+Shiro实现登陆拦截功能的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue 路由跳转打开新窗口(被浏览器拦截
- 下一篇: mysql的to char data_数