前言:相信点进来的同窗大部分是刚接触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安全框架的登陆验证简单实现;
以为还能够请点个赞,赞不了也能够收藏;
总之,谢谢阅读~