前言:相信點進來的同窗大部分是剛接觸shiro框架
因此咱們從最基礎開始,固然我會拋開那些shiro的官方圖(真的有人會認真看那玩意兒?),一步步向你們講解shiro的配置過程及登陸認證的簡單實現前端
Shiro是用來幫助咱們作權限管理的,本篇文章的shiro使用在Web項目上,因此我用了最新的spring boot爲框架。(固然使用xml來進行配置也能夠,原理是同樣的,只是寫法不一樣)java
在開始學習以前理解什麼是shiro的權限管理?
咱們知道shiro的主要功能有認證,受權,加密,會話管理,緩存等
一大堆功能會讓你以爲學起來毫無胃口,這裏咱們主要知道什麼是認證,受權就行mysql
(這樣理解確定不許確,可是更易懂)
認證就是登陸認證:你登陸了這個網頁,shiro會經過一個口令(這裏咱們用token)來認證你,固然你也會用這個口令去獲得服務器的承認,進行後續的權限操做;
受權就是權限受理:shiro會根據你提供的信息進行認證以後,給予你相應的權力(如刪除,添加等);web
要記住Shiro不會給你建立和維護關係表,須要咱們本身在數據庫建立出對應的關係表:用戶——角色——權限
讓咱們看下這幾張表:
1.user(用戶表)ajax
2.role(角色表)spring
3.permission(權限表)sql
用戶和角色是一對多的關係,一個用戶能夠擁有多個角色(好比管理員,普通用戶)
角色和權限是多對多的關係,一個角色能夠用個多個權限,一個權限也能對應多個用戶
固然還有關聯表,這裏很少說,由於咱們只作登陸驗證,因此目前只須要一張用戶表便可數據庫
那麼什麼是登陸認證,我想不少初學者會曲解它的意思,它並非幫助你去登陸用戶名帳號的。
要真正理解它,咱們就須要知道shiro是用來幹什麼的?登陸認證在shiro中起什麼做用?apache
前面說了shiro是用來作權限管理的,而登陸以後怎樣才能讓shiro一直記得你,這就是登陸認證的做用
那麼有同窗就會問,爲何要用shiro的認證,而不去使用數據庫的用戶表來認證?
這個問題我也問過,繼續理解便會知道:
由於你以後的每次操做都要用服務端返回給你的數據來校驗,若是使用User表數據是極不安全和不可靠的,既然加入了shiro框架,就要考慮到安全性,因此咱們會使用token來進行校驗,這也是本篇文章的重點!json
廢話很少說,咱們開始吧:
這裏我使用maven來進行包的管理:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.lxt</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>demo</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.8.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!--spring boot--> <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-thymeleaf</artifactId> <version>1.5.8.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>1.5.8.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>1.5.8.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> <version>1.5.8.RELEASE</version> </dependency> <!--熱部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <version>1.5.8.RELEASE</version> <optional>true</optional> <scope>true</scope> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.5</version> </dependency> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.5</version> </dependency> <!--aop--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>1.5.8.RELEASE</version> </dependency> <!--junit--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.5</version> </dependency> <!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-cas</artifactId> <version>1.3.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> </plugin> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.5</version> <configuration> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.30</version> </dependency> </dependencies> </plugin> </plugins> </build> </project>
pom配置好以後,咱們就要用java編寫shiro的全局配置類。
在配置shiro以前咱們須要明白它的三大要素:
Subject:單個對象,與如何應用交互的用戶對象;
SecurityManager:安全管理器,管理Subject;
Realm:域,SecurityManager與Realm交互得到數據(用戶-角色-權限)
知道這些後咱們開始新建一個ShiroConfig類:
(由於本篇只學習登陸認證,因此咱們先不用緩存管理,密碼編碼等功能)
package cn.lxt.shiro; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class shiroConfig { /** * 負責shiroBean的生命週期 */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ return new LifecycleBeanPostProcessor(); } /** *這是個自定義的認證類,繼承子AuthorizingRealm,負責用戶的認證和權限處理 */ @Bean @DependsOn("lifecycleBeanPostProcessor") public MyShiroRealm shiroRealm(){ MyShiroRealm realm = new MyShiroRealm(); //realm.setCredentialsMatcher(hashedCredentialsMatcher()); return realm; } /** 安全管理器 * 將realm加入securityManager * @return */ @Bean public SecurityManager securityManager(){ //注意是DefaultWebSecurityManager!!! DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm()); return securityManager; } /** shiro filter 工廠類 * 1.定義ShiroFilterFactoryBean * 2.設置SecurityManager * 3.配置攔截器 * 4.返回定義ShiroFilterFactoryBean */ @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){ //1 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //2 //註冊securityManager shiroFilterFactoryBean.setSecurityManager(securityManager); System.out.println("11"); //3 // 攔截器+配置登陸和登陸成功以後的url //LinkHashMap是有序的,shiro會根據添加的順序進行攔截 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); //配置不會被攔截的鏈接 這裏順序判斷 //anon,全部的url均可以匿名訪問 //authc:全部url都必須認證經過才能夠訪問 //user,配置記住我或者認證經過才能訪問 //logout,退出登陸 filterChainDefinitionMap.put("/JQuery/**","anon"); filterChainDefinitionMap.put("/js/**","anon"); //配置退出過濾器 filterChainDefinitionMap.put("/example1","anon"); filterChainDefinitionMap.put("/lxt","anon"); filterChainDefinitionMap.put("/login","authc"); filterChainDefinitionMap.put("/success","anon"); filterChainDefinitionMap.put("/index","anon"); filterChainDefinitionMap.put("/Register","anon"); filterChainDefinitionMap.put("/logout","logout"); //過濾鏈接自定義,從上往下順序執行,因此用LinkHashMap /**放在最下邊 filterChainDefinitionMap.put("/**","authc"); //設置登陸界面,若是不設置爲尋找web根目錄下的文件 shiroFilterFactoryBean.setLoginUrl("/lxt"); //設置登陸成功後要跳轉的鏈接 shiroFilterFactoryBean.setSuccessUrl("/success"); //設置登陸未成功,也能夠說無權限界面 shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); System.out.println("shiro攔截工廠注入類成功"); //4 //返回 return shiroFilterFactoryBean; } }
以上須要注意幾點:
1.shiroFilter是入口,主要有四步操做,代碼中已經註釋清楚
2.shiroFilterFactoryBean.setLoginUrl("/lxt");啓動類無論你輸入怎樣的url,他都會跳轉到登陸啓動類;
3.shiroFilterFactoryBean.setSuccessUrl("/success");登陸成功後跳轉的類,這個方法你們能夠不用管,由於我感受它根本用不到,大神別噴!
看完了ShiroConfig類以後,許多人會問:噫!個人MyShiroRealm怎麼導入不進來!
其實這個方法的調用須要咱們本身再寫一個Realm類繼承AuthorizingRealm。
繼承以後咱們須要重寫兩個方法:
1.doGetAuthorizationInfo()方法用於角色和權限的控制,暫不使用;
2.doGetAuthenticationInfo()方法用於登陸認證,重點。
下面貼出代碼:
package cn.lxt.shiro; import cn.lxt.bean.User; import cn.lxt.service.UsersService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; public class MyShiroRealm extends AuthorizingRealm { @Autowired private UsersService usersService; /** * 用於獲取登陸成功後的角色、權限等信息 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } /** * 驗證當前登陸的Subject * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //拿到帳號(username) String username = (String) token.getPrincipal(); System.out.println("username=:"+username); //檢查token的信息 System.out.println(token.getCredentials()); User user = usersService.findByName(username); if (user==null){ return null; } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),getName()); return info; } }
經過以上代碼你會發現,咱們是怎樣進行驗證的,進行驗證的關係點是傳入的參數token
如今你們應該明白了token在本篇文章中的做用!
固然有些同窗看到這裏仍是雲裏霧裏,在這我稍微講解一些思路:
1.當咱們進行帳號密碼登陸的時候,會建立一個token(token只是一種概念,具體的實現仍是要定義的)到數據庫;
2.token存入的時候綁定了登陸傳入的用戶名和密碼(token又不少實現類,推薦使用UsernamePasswordToken);
3.shiro自帶的框架會將token與SimpleAuthenticationInfo類對象進行比較,失敗拋出指定異常(須要本身捕獲)
完成上面shiroFactory和realm的配置以後;
咱們就要真正的去調用shiro的認證功能了
要明白,在shiro的登陸認證中:
Controller幫你獲取post參數後,
進行參數綁定,再調用subject.login()方法;
若是用戶名密碼正確,會跳轉SuccessUrl,
因此說Controller獲取參數後注入給Shiro,信息錯誤則在Controller中報錯
@PostMapping(value = "testLogin") public Map<String,Object> testLogin(@RequestParam("name")String name,@RequestParam("password")String password){ Map<String,Object> map = new HashMap<String,Object>(); //建立subject實例 Subject subject = SecurityUtils.getSubject(); //判斷當前的subject是否登陸 if (subject.isAuthenticated()==false){ //將用戶名和密碼存入UsernamePasswordToken中 UsernamePasswordToken token = new UsernamePasswordToken(name,password); try { //將存有用戶名和密碼的token存進subject中 subject.login(token); }catch (UnknownAccountException uae){ System.out.println("沒有用戶名爲"+token.getPrincipal()+"的用戶"); } catch (IncorrectCredentialsException ice){ System.out.println("用戶名爲:"+token.getPrincipal()+"的用戶密碼不正確"); } catch (LockedAccountException lae){ System.out.println("用戶名爲:"+token.getPrincipal()+"的用戶已被凍結"); } catch (AuthenticationException e){ System.out.println("未知錯誤!"); } } return "success"; }
以上只是在springmvc中的shiro實現,
可是實際開發中,先後端分離愈來愈流行,
分離以後的RestFulApi咱們要怎麼實現shiro呢?
在這裏個人想法是本身建立token
RestFul下的思路:
1.當咱們進行帳號密碼登陸的時候,會建立一個token(UUID隨機生成)
2.token存入的時候要記得它是隨機生成的,生成以後會與用戶登陸的id進行綁定;
3.咱們登陸完成以後,返回給瀏覽器的JSON對象要包含token值,瀏覽器會把token值存入到瀏覽器中。
思路清楚以後咱們要進行實現:
1.建立token:
package cn.lxt.controller; import cn.lxt.bean.User; import cn.lxt.service.TokenService; import cn.lxt.service.UsersService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.Map; @Controller public class LoginController { @Autowired private UserService userService; @Autowired private TokenService tokenService; @ApiOperation(value = "登陸驗證",notes = "成功返回200,失敗返回500,返回一個TokenJSON對象") @ApiImplicitParams({ @ApiImplicitParam(name = "name",value = "帳號名",required = true,dataType = "String"), @ApiImplicitParam(name = "password",value = "密碼",required = true,dataType = "String") }) @RequestMapping(value = "/ajaxLogin",method = RequestMethod.POST) public Map<String, Object> ajaxLogin(@RequestParam("name")String name, @RequestParam("password")String password){ tokenService.checkExpire(); Map<String, Object> map = new HashMap<String,Object>(); User user = new User(name,password); int status = userService.queryUser(user); if (status==200){ map = tokenService.createToken(user); } map.put("status",status); return map; } }
在controller中返回一個User和Token給前端;
2.在Service中建立token,而且存入數據庫:
package cn.lxt.service.Impl; import cn.lxt.bean.Token; import cn.lxt.dao.TokenMapper; import cn.lxt.service.TokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.UUID; @Service public class TokenServiceImple implements TokenService{ private static final int Expire = 3600*25; @Autowired private UserMapper userMapper; @Autowired private TokenMapper tokenMapper; @Override public Map<String, Object> createToken(User user) { User user1 = userMapper.selectByNameAndPassword(user); //建立TokenEntity參數 String newtoken = UUID.randomUUID().toString(); Date updateTime = new Date(); Date expireTime = new Date(updateTime.getTime()+Expire*1000); Token token = new Token(newtoken,user1.getId(),updateTime,expireTime); //判斷token是否已經存在,不存在就存入,存在就更新 if (tokenMapper.findByUserId(user1.getId())==null){ tokenMapper.insert(token); System.out.println("存入成功"); }else { tokenMapper.updateByToken(token); System.out.println("更新成功"); } Map<String,Object> map = new HashMap<String,Object>(); map.put("token",token); return map; } @Override public void checkExpire() { Date now = new Date(); List<Token> list = tokenMapper.selectByExample(new TokenExample()); for (Token token:list){ if (token.getExpiretime().getTime()<now.getTime()){ tokenMapper.deleteByExpireTime(token); System.out.println(token.getTokenid()+"已刪除"); } } } }
上面建立token的時候由於時間緣由沒有判斷用戶Id的token是否已在數據庫存在,大家能夠本身試下;
3.OK,咱們token已經建立了,而且把它以JSON的格式穿了過去,如今要作的就是把token存到瀏覽器中:
在登陸界面的登陸按鈕上,咱們設置一個js方法:
function login() { var name = document.getElementById('name').value; console.log(name); var password=document.getElementById('password').value;; var url='http://localhost:8088/ajaxLogin' $.ajax({ url:url, type:'post', data:{name:name,password:password}, datatype:'json', success:function (result) { if(result.status==200){ localStorage.setItem("token",result.token) console.log(result) }else if(result.status=500){ alert('登陸失敗!') } } }) }
上面代碼把token傳進localStorage中了。
可是,細心的同窗會發現,雖然存進了localStorage中,可是從請求頭傳給後端是最優解決方案,也就是須要將token附加在Header裏,並且咱們要作到訪問任意url,都能把token從localStorage轉存到Header中,這個問題就交給機智的大家了,若是實在作不出來能夠私信我
以上即是Spring Boot上Shiro安全框架的登陸驗證簡單實現;
以爲還能夠請點個贊,贊不了也能夠收藏;
總之,謝謝閱讀~