安全框架--shiro
0.2 名词及含义
SecurityManager:安全管理器,由框架提供的,整个shiro框架最核心的组件。
Realm:安全数据桥,类似于项目中的DAO,访问安全数据的,框架提供,开发人员也可自己编写
0.3 网上关于shiro的资料
https://www.2cto.com/kf/201604/502563.html
https://blog.csdn.net/m0_38053538/article/details/80965359
1.前后端分离的登陆
shiro总结:
1.shiro登录
前台登录 --> loginController --> 完成登录认证 --> token(jsessionid)带到前端去 --> 每次发送请求过来(jsessionid带过来) --> 登录认证过滤器 --> shiro重写getSession的方法 --> 访问数据
2.shiro授权
前台登录 --> 授权过滤器(处理没有权限的时候,返回json格式) --> map(查询数据库权限交给shiro管理) --> 判断当前用户操作数据是否有权限(realm) --> 如果查询出来没有权限 --> 提示没有权限
1.1 shiro概念回顾
shiro是安全的框架,轻量级,Apache公司的,它具备身份认证 授权,密码学和会话管理
spring security也是安全框架, 重量级 (可以和spring很好融合),spring公司的
1.2 业务流程
LoginController层:
1.获取subject(主体)
2.判断主体是否认证通过
认证过:可以访问
认证不过:去认证
通过UserNameAndPassWordToken 去获得: token(令牌)
调用subject.login(token),去完成认证
3.调用到对应realm
取出数据库密码
把用户名和密码交给shiro ,去认证
4.把信息保存session里面
5.执行其他操作
1.2.1 搭建shiro环境
1.引入分层依赖
shiro层(因为需要查询数据库)
<dependency>
<groupId>cn.itsource</groupId>
<artifactId>crm_service</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
web.controller层:也需要shiro的依赖
<dependency>
<groupId>cn.itsource</groupId>
<artifactId>crm_shiro</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2.导入shiro需要的jar包:shiro模块下,pom.xml
<!--shiro的jar包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.4.1</version>
</dependency>
<!--shiro的依赖包-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
3.web.xml,加入过滤器(因为代理过滤器需要找到真实过滤器)
<!--shiro需要找到真实过滤器-->
<!--shiro代理过滤器-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
4.applicationContext-shiro.xml配置文件
Itsource_auth_shiro中的applicationContext-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<!--shiro的核心对象-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--配置realm-->
<property name="realm" ref="authRealm"/>
</bean>
<!--Realms-->
<bean id="authRealm" class="cn.itsource.shiro.realm.AuthenRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="10"/>
</bean>
</property>
</bean>
<!--shiro的过滤器配置 web.xml的代理过滤器名称一样-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/s/login"/>
<property name="successUrl" value="/s/index"/>
<property name="unauthorizedUrl" value="/s/unauthorized"/>
<property name="filterChainDefinitions">
<value>
/login = anon
/** = authc
</value>
</property>
</bean>
</beans>
5.创建一个realm自定义的类,做认证功能:
public class AuthenRealm extends AuthorizingRealm {
@Autowired
private IEmployeeService employeeService;
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//得到token的令牌
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 取到用户名
String username = token.getUsername();
//从数据库查询用户
Employee employee = employeeService.getByUsername(username);
if(employee==null){
throw new UnknownAccountException(username);
}
//主体
Object principal = employee;
//得到数据库密码
Object hashedCredentials = employee.getPassword();
//准备一个颜值
ByteSource credentialsSalt = ByteSource.Util.bytes(MD5Util.SALT);
String realmName = getName();
//把从数据库查询出来的信息和当前信息做比较
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,hashedCredentials,credentialsSalt,realmName);
return info;
}
}
6.把配置文件集成到spring,web.xml中
增加如下:
classpath*:applicationContext-shiro.xml
结果如下:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:applicationContext.xml,
classpath*:applicationContext-shiro.xml
</param-value>
</context-param>
<!--Spring监听器 ApplicationContext 载入 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
7.创建MD5Util类,给字符串加密加盐
shiro类里面写一个包util,写一个类:MD5Util
package cn.itsource.shiro.util;
import org.apache.shiro.crypto.hash.SimpleHash;
public class MD5Util {
public static final String SALT = "itsource";
/**
* 加密
* @param source:需要加密的字符串
* @return
*/
public static String encrypt(String source){
//参数:加密的名字,要加密的字符串,盐值,加密次数
SimpleHash simpleHash = new SimpleHash("MD5",source,SALT,10);
return simpleHash.toString();
}
public static void main(String[] args) {
System.out.println(encrypt("1"));
}
}
1.3 登录实现
1.3.1 前台
(1).准备登陆页面
(2)点击登录按钮,发送请求方法
1.login.vue
handleSubmit2(ev) {
var _this = this;
this.$refs.ruleForm2.validate((valid) => {
if (valid) {
//_this.$router.replace('/table');
this.logining = true;
//NProgress.start();
var loginParams = { username: this.ruleForm2.account, password: this.ruleForm2.checkPass };
this.$http.post("/login",loginParams).then(data => {
this.logining = false;
let { message, success, resultObj } = data.data;
if (!success) {
this.$message({
message: message,
type: 'error'
});
} else {
//登录成功跳转/table的路由地址
sessionStorage.setItem('user', JSON.stringify(resultObj));
// this.$router.push({ path: '/table' });
//修改登录成功后跳转到首页
this.$router.push({ path: '/echarts' });
}
});
} else {
console.log('error submit!!');
return false;
}
});
}
2.在main.js中解开之前注释掉的登录
/*
登录权限判断
router.beforeEach((to, from, next) => {
//NProgress.start();
if (to.path == '/login') {
sessionStorage.removeItem('user');
}
let user = JSON.parse(sessionStorage.getItem('user'));
if (!user && to.path != '/login') {
next({ path: '/login' })
} else {
next()
}
})*/
1.3.2 继续后台
1.LoginController实现登录
@Controller
@CrossOrigin
public class LoginController {
/**
* 身份认证--登录
* @param employee
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
@ResponseBody
public AjaxResult login(@RequestBody Employee employee){
//1.获得主体
Subject subject = SecurityUtils.getSubject();
//通过主体判断是否认证过
if (!subject.isAuthenticated()){
//没有认证过
//通过账号密码去获得令牌
String username = employee.getUsername();
String password = employee.getPassword();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
//通过UsernamePasswordToken认证
try{
//调用该方法,即时调用自定义的realm类
subject.login(usernamePasswordToken);
//不知道账户异常,账号错误
}catch (UnknownAccountException e){
e.printStackTrace();
return new AjaxResult("不知道账户异常,账号错误");
//错误凭证异常,密码错误
}catch (IncorrectCredentialsException e){
e.printStackTrace();
return new AjaxResult("错误凭证异常,密码错误");
//认证异常,其他错误
}catch (AuthenticationException e){
e.printStackTrace();
return new AjaxResult("认证异常,其他错误");
}
}
Employee employee1 = (Employee) subject.getPrincipal();
employee.setPassword(null);
//除了返回登录成功与否,还要把登录的用户返回前端,放到前台的session
return AjaxResult.me().setResultObj(employee1);
}
}
2.AjaxResult:添加字段
因为前台需要success、message字段,还有对象信息,需要AjaxResult增加字段
AjaxResult类中,添加代码:
public AjaxResult setResultObj(Object resulObj) {
this.resultObj = resultObj;
return this;
}
完整内容:
/**
* Ajax请求的返回内容:增删改
* success:成功与否
* message:失败原因
*/
public class AjaxResult {
private boolean success = true;
private String message = "操作成功!";
private Object resultObj = null;
public boolean isSuccess() {
return success;
}
//链式编程,可以继续. 设置完成后自己对象返回
public AjaxResult setSuccess(boolean success) {
this.success = success;
return this;
}
public String getMessage() {
return message;
}
public AjaxResult setMessage(String message) {
this.message = message;
return this;
}
//默认成功
public AjaxResult() {
}
//失败调用
public AjaxResult(String message) {
this.success = false;
this.message = message;
}
public Object getResultObj() {
return resultObj;
}
public AjaxResult setResultObj(Object resulObj) {
this.resultObj = resultObj;
return this;
}
//不要让我创建太多对象
public static AjaxResult me(){
return new AjaxResult();
}
public static void main(String[] args) {
AjaxResult.me().setMessage("xxx").setSuccess(false);
}
}
4.Service层:
IEmployeeService
public interface IEmployeeService extends IBaseService<Employee> {
/**
* 添加租户员工
* @param employee
*/
void addTenantEmployee(Employee employee);
Employee getByUsername(String username);
}
EmployeeServiceImpl :
@Service
public class EmployeeServiceImpl extends BaseServiceImpl<Employee> implements IEmployeeService {
@Autowired
private TenantMapper tenantMapper;
@Autowired
private EmployeeMapper employeeMapper;
@Override
public void addTenantEmployee(Employee employee) {
Tenant tenant = employee.getTenant();
tenant.setRegisterTime(new Date());
tenant.setState(0);
//添加租户返回租户id 添加前对象里面没有id,添加完成后就有了
tenantMapper.save(tenant);
//把租户id设置给员工
employee.setTenant(tenant);
//在保存员工
employee.setRealName(employee.getUsername());
employeeMapper.save(employee);
}
@Override
public Employee getByUsername(String username) {
return employeeMapper.loadByUsername(username);
}
}
5.Mapper
EmployeeMapper :
/**
* 通过继承baseMapper拥有的基础crud,还可以扩展自己方法
*/
public interface EmployeeMapper extends BaseMapper<Employee> {
Employee loadByUsername(String username);
}
EmployeeMapper .xml
<!--Employee loadByUsername(String username);-->
<select id="loadByUsername" parameterType="string" resultType="Employee">
select * from t_employee WHERE username = #{username}
</select>
1.2.4 问题:成功后无法访问
1.2.4.1原因分析:
前后端分离项目中,ajax请求没有携带cookie,所以后台无法通过cookie获取到SESSIONID,从而无法获取到session对象。而shiro的认证与授权都是通过session实现的。
解决办法:登录成功后返回token,并以后每次ajax请求都携带token
1.2.4.2 后端代码实现
1)登录成功后返回token,并以后每次ajax请求都要携带token
1.LoginController中
//课件中的代码:
Employee employee1 = (Employee) currentUser.getPrincipal();
employee.setPassword(null);
Map<String,Object> result = new HashMap<>();
result.put("user",employee1);
System.out.println(currentUser.getSession().getId()+"xxxx"); 登录成功后把会话id返回,会后作为token使用
result.put("token",currentUser.getSession().getId());
return AjaxResult.me().setResultObj(result);
//老师写的代码:
//把employee信息传到前台,前台放入session(前台session)
Employee employee1 = (Employee)subject.getPrincipal();
UserContext.setUser(employee1);
AjaxResult ajaxResult = new AjaxResult();
Map mp = new HashMap<>();
mp.put("user",employee1);
//jsessionid -->token
mp.put("token",subject.getSession().getId());
ajaxResult.setResultObj(mp);
//返回对象
1.2.4.3 前端代码实现
1.Longin.vue中,登录后跳转
this.$http.post("/login",loginParams).then(data => {
this.logining = false;
let { success, message, resultObj } = data.data;
if (!success) {
this.$message({
message: message,
type: 'error'
});
} else {
console.log(resultObj)
//登录成功跳转/table的路由地址
sessionStorage.setItem('user', JSON.stringify(resultObj.user));
sessionStorage.setItem('token', resultObj.token); //不要加字符串转换了巨大的坑
//修改登录成功后跳转到首页
this.$router.push({ path: '/echarts' });
}
2.Home.vue中,退出登录
//退出登录
logout: function () {
var _this = this;
this.$confirm('确认退出吗?', '提示', {
//type: 'warning'
}).then(() => {
sessionStorage.removeItem('user');
sessionStorage.removeItem('token');
_this.$router.push('/login');
}).catch(() => {
});
3.Main.js中,每次请求都拦截,把x-token设置到请求头中
//拦截器
axios.interceptors.request.use(config => {
if (sessionStorage.getItem('token')) {
// 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
config.headers['X-Token'] = sessionStorage.getItem('token')
}
console.debug('config',config)
return config
}, error => {
// Do something with request error
Promise.reject(error)
})
1.2.4.3 后端代码实现
服务端变为通过token来唯一标识session
1.Shiro spring配置文件中,applicationContext-shiro.xml中:
<!--session管理器-->
<bean id="sessionManager" class="cn.itsource.shiro.util.CrmSessionManager"/>
<!--shiro的核心对象-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="sessionManager" ref="sessionManager"/>
<!--配置realm-->
<property name="realm" ref="authRealm"/>
</bean>
2.创建CrmSessionManager类,并继承DefaultWebSessionManager类
如果请求到后台没有sessionid,则设置sessionid,有则获取sessionid
package cn.itsource.shiro.util;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
/**
*
* 传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,
* 在前后端分离的项目中(也可在移动APP项目使用),我们选择在ajax的请求头中传递sessionId,
* 因此需要重写shiro获取sessionId的方式。
* 自定义CrmSessionManager类继承DefaultWebSessionManager类,重写getSessionId方法
*
*/
public class CrmSessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "X-TOKEN";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public CrmSessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//取到jessionid
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
HttpServletRequest request1 = (HttpServletRequest) request;
//如果请求头中有 X-TOKEN 则其值为sessionId
if (!StringUtils.isEmpty(id)) {
System.out.println(id+"jjjjjjjjj"+request1.getRequestURI()+request1.getMethod());
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
3.xml中
跨域预检查放行:设置OPTIONS请求的放行
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<bean id="myAuthc" class="cn.itsource.shiro.util.MyAuthenticationFilter"/>
<!--shiro的过滤器配置-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/s/login"/>
<property name="successUrl" value="/s/index"/>
<property name="unauthorizedUrl" value="/s/unauthorized"/>
<property name="filters">
<map>
<entry key="myAuthc" value-ref="myAuthc"/>
</map>
</property>
<property name="filterChainDefinitions">
<value>
/login = anon
/** = myAuthc
</value>
</property>
</bean>
创建MyAuthenticationFilter类,继承FormAuthenticationFilter类,覆写isAccessAllowed方法,重新设置放行方法
package cn.itsource.shiro.util;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义身份认证过滤器
*/
public class MyAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//如果是OPTIONS请求,直接放行
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String method = httpServletRequest.getMethod();
System.out.println(method);
if("OPTIONS".equalsIgnoreCase(method)){
return true;
}
return super.isAccessAllowed(request, response, mappedValue);
}
}
1.4 抽取登录用户的代码
1.创建UserContext类,专门用来登录调用
package cn.itsource.shiro.util;
import cn.itsource.domain.Employee;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
/**
* 当前登录用户相关
*/
public class UserContext {
private static final String CURRENT_LOGIN_USER= "loginUser";
/**
* 设置当前登录用户
* @param employee
*/
public static void setUser(Employee employee){
Subject currentUser = SecurityUtils.getSubject();
currentUser.getSession().setAttribute(CURRENT_LOGIN_USER,employee);
}
/**
* 获取当前登录用户
* @return employee
*/
public static Employee getUser(){
Subject currentUser = SecurityUtils.getSubject();
return (Employee) currentUser.getSession().getAttribute(CURRENT_LOGIN_USER);
}
}
2.现在登录的代码:
//以下获取当前登录用户存在问题如下:
//1 到处都散落获取当前登录用户代码
//2 以后不用shiro所有的地方都要改变
//解决方案:封装一个方法获取当前登录用户,以后变了只需要修改这个方法就ok了
Subject currentUser = SecurityUtils.getSubject();
Object loginUser = currentUser.getSession().getAttribute("loginUser");