Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
参考https://segmentfault.com/a/1190000011918957
Shiro权限控制之登录认证
shiro的主要功能有认证,授权,加密,会话管理,缓存等
一大堆功能会让你觉得学起来毫无胃口,这里我们主要知道什么是认证,授权就行。
认证就是登录认证:你登录了这个网页,shiro会通过一个口令(这里我们用token)来认证你,当然你也会用这个口令去得到服务器的认可,进行后续的权限操作;
授权就是权限受理:shiro会根据你提供的信息进行认证之后,给予你相应的权力(如删除,添加等);
要记住Shiro不会给你创建和维护关系表,需要我们自己在数据库创建出对应的关系表:用户——角色——权限
让我们看下这几张表:
-
user(用户表)
-
role(角色表)
- permission(权限表)
用户和角色是一对多的关系,一个用户可以拥有多个角色(比如管理员,普通用户)
角色和权限是多对多的关系,一个角色可以用个多个权限,一个权限也能对应多个用户
当然还有关联表,这里不多说,因为我们只做登录验证,所以目前只需要一张用户表即可
那么什么是登录认证,我想很多初学者会曲解它的意思,它并不是帮助你去登录用户名账号的。
要真正理解它,我们就需要知道shiro是用来干什么的?登录认证在shiro中起什么作用?
前面说了shiro是用来做权限管理的,而登录之后怎样才能让shiro一直记得你,这就是登录认证的作用
那么有同学就会问,为什么要用shiro的认证,而不去使用数据库的用户表来认证?
这个问题我也问过,继续理解便会知道:
因为你之后的每次操作都要用服务端返回给你的数据来校验,如果使用User表数据是极不安全和不可靠的,既然加入了shiro框架,就要考虑到安全性,所以我们会使用token来进行校验,这也是本篇文章的重点!
第一步:引入相关包
这里我使用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>
第二步:配置Shiro
pom配置好之后,我们就要用java编写shiro的全局配置类。
在配置shiro之前我们需要明白它的三大要素:
Subject:单个对象,与如何应用交互的用户对象;
SecurityManager:安全管理器,管理Subject;
Realm:域,SecurityManager与Realm交互获得数据(用户-角色-权限)
知道这些后我们开始新建一个ShiroConfig类:
(因为本篇只学习登录认证,所以我们先不用缓存管理,密码编码等功能)
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");登录成功后跳转的类,这个方法大家可以不用管,因为我感觉它根本用不到,大神别喷!
第三步:配置Realm
看完了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类对象进行比较,失败抛出指定异常(需要自己捕获)
第四步:Controller的编写
完成上面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";
}
第五步:在Restful风格下的实现
以上只是在springmvc中的shiro实现,
但是实际开发中,前后端分离越来越流行,
分离之后的RestFulApi我们要怎么实现shiro呢?
在这里我的想法是自己创建token
RestFul下的思路:
1.当我们进行账号密码登录的时候,会创建一个token(UUID随机生成)
2.token存入的时候要记得它是随机生成的,生成之后会与用户登录的id进行绑定;
3.我们登录完成之后,返回给浏览器的JSON对象要包含token值,浏览器会把token值存入到浏览器中。
思路清楚之后我们要进行实现:
- 创建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给前端;
- 在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是否已在数据库存在,你们可以自己试下;
- 我们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中,这个问题就交给机智的你们了-_-