代表当前与程序进行交互的使用者
是 Shiro 架构的心脏,并作为一种“保护伞”对象来协调内部的安全组件共同构成一个对象图
一个用于认证和授权的类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 目的: 跨域访问控制
* 做前后分离的话,这个也是必配的
* 备注说明:
*/
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许任何域名使用
corsConfiguration.addAllowedOrigin("*");
// 允许任何头
corsConfiguration.addAllowedHeader("*");
// 允许任何方法(post、get等)
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 对接口配置跨域设置
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
package com.example.demo;
import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* 目的: 过滤OPTIONS请求
* 继承shiro 的form表单过滤器,对 OPTIONS 请求进行过滤。
* 前后端分离项目中,由于跨域,会导致复杂请求,即会发送preflighted request,这样会导致在GET/POST等请求之前会先发一个OPTIONS请求,但OPTIONS请求并不带shiro
* 的'authToken'字段(shiro的SessionId),即OPTIONS请求不能通过shiro验证,会返回未认证的信息。
*
* 备注说明: 需要在 shiroConfig 进行注册
*/
public class CORSAuthenticationFilter extends FormAuthenticationFilter {
/**
* 直接过滤可以访问的请求类型
*/
private static final String REQUET_TYPE = "OPTIONS";
public CORSAuthenticationFilter() {
super();
}
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (((HttpServletRequest) request).getMethod().toUpperCase().equals(REQUET_TYPE)) {
return true;
}
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse res = (HttpServletResponse)response;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setStatus(HttpServletResponse.SC_OK);
res.setCharacterEncoding("UTF-8");
PrintWriter writer = res.getWriter();
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg","请先登录");
writer.write(String.valueOf(jsonObject));
writer.close();
return false;
}
}
import lombok.Data;
@Data
@Entity
@Table
public class User {
private String username;
private String password;
}
这张表是一个数据库实体,在登录认证时查询数据库比对传入的username 和password是否正确
package com.example.demo;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class MyRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
这里没有查数据库
在实际项目中需要查数据进行验证
User user = new User();
user.setPassword("123");
user.setUsername("123");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
if (user == null){
throw new UnknownAccountException("没有该用户");
}
if (!token.getUsername().equals(user.getUsername()) || !token.getPassword().equals(user.getPassword())){
throw new IncorrectCredentialsException("用户名或密码不正确") ;
}
return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
}
}
package com.example.demo;
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 java.io.Serializable;
/**
* 目的: shiro 的 session 管理
* 自定义session规则,实现前后分离,在跨域等情况下使用token 方式进行登录验证才需要,否则没必须使用本类。
* shiro默认使用 ServletContainerSessionManager 来做 session 管理,它是依赖于浏览器的 cookie 来维护 session 的,
* 调用 storeSessionId 方法保存sesionId 到 cookie中
* 为了支持无状态会话,我们就需要继承 DefaultWebSessionManager
* 自定义生成sessionId 则要实现 SessionIdGenerator
*
* @author 小鸟的胖次
*/
public class ShiroSession extends DefaultWebSessionManager {
/**
* 定义的请求头中使用的标记key,用来传递 token
*/
private static final String AUTH_TOKEN = "authToken";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public ShiroSession() {
super();
//设置 shiro session 失效时间,默认为30分钟,这里现在设置为15分钟
//setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15);
}
/**
* 获取sessionId,原本是根据sessionKey来获取一个sessionId
* 重写的部分多了一个把获取到的token设置到request的部分。这是因为app调用登陆接口的时候,是没有token的,登陆成功后,产生了token,我们把它放到request中,返回结
* 果给客户端的时候,把它从request中取出来,并且传递给客户端,客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了
* @param request ServletRequest
* @param response ServletResponse
* @return Serializable
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取请求头中的 AUTH_TOKEN 的值,如果请求头中有 AUTH_TOKEN 则其值为sessionId。shiro就是通过sessionId 来控制的
String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN);
if (StringUtils.isEmpty(sessionId)){
//如果没有携带id参数则按照父类的方式在cookie进行获取sessionId
return super.getSessionId(request, response);
} else {
//请求头中如果有 authToken, 则其值为sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
//sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}
}
}
创建ShiroConfig类
package com.example.demo;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
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.context.annotation.Bean;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 描述 shiro的配置类
*
* @author 小鸟的胖次
*/
@Configuration
public class ShiroConfig {
/**
* 对shiro的拦截器进行注入
*
* securityManager:
* 所有Subject 实例都必须绑定到一个SecurityManager上,SecurityManager 是 Shiro的核心,初始化时协调各个模块运行。然而,一旦 SecurityManager协调完毕,
* SecurityManager 会被单独留下,且我们只需要去操作Subject即可,无需操作SecurityManager 。 但是我们得知道,当我们正与一个 Subject 进行交互时,实质上是
* SecurityManager在处理 Subject 安全操作
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map customFilterMap = new LinkedHashMap<>();
customFilterMap.put("corsAuthenticationFilter", new CORSAuthenticationFilter());
shiroFilter.setFilters(customFilterMap);
//拦截器,配置访问权限 必须是LinkedHashMap,因为它必须保证有序。滤链定义,从上向下顺序执行,一般将 /**放在最为下边
Map filterMap = new LinkedHashMap();
filterMap.put("/login", "anon");
//剩余的请求shiro都拦截
filterMap.put("/**/*", "authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
// 配置复杂请求过滤器
return shiroFilter;
}
/**
* securityManager 核心配置
* 安全控制层
* @return
*/
@Bean
public org.apache.shiro.mgt.SecurityManager securityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//设置自定义的realm
defaultWebSecurityManager.setRealm(myRealm());
//自定义的shiro session 缓存管理器
defaultWebSecurityManager.setSessionManager(sessionManager());
return defaultWebSecurityManager;
}
/**
* 自定义的realm
* @return
*/
@Bean
public MyRealm myRealm() {
return new MyRealm();
}
/**
* 开启shiro 的AOP注解支持
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 自定义的 shiro session 缓存管理器,用于跨域等情况下使用 token 进行验证,不依赖于sessionId
* @return
*/
@Bean
public SessionManager sessionManager(){
//将我们继承后重写的shiro session 注册
ShiroSession shiroSession = new ShiroSession();
//如果后续考虑多tomcat部署应用,可以使用shiro-redis开源插件来做session 的控制,或者nginx 的负载均衡
shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO());
return shiroSession;
}
}
package com.example.demo;
import org.apache.catalina.security.SecurityUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;
import java.io.Serializable;
/**
* @author 小鸟的胖次
*/
@RequestMapping("/")
@org.springframework.stereotype.Controller
public class Controller {
@GetMapping("/user")
@ResponseBody
public String user (){
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
System.out.println("当前操作者username:" + user.getUsername());
return "成功访问到user请求";
}
@PostMapping("/login")
@ResponseBody
public String login (String username , String password){
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
SecurityUtils.getSubject().login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
}
Subject subject = SecurityUtils.getSubject();
Serializable tokenId = subject.getSession().getId();
return String.valueOf(tokenId);
}
@GetMapping("/list")
@ResponseBody
public String list (){
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
System.out.println("当前操作者username:" + user.getUsername());
return "成功访问到list请求";
}
}
登录失败,后端将我们的请求重定向到login.jsp页面但是我们没有写 所以有404错误
可以看到是谁登录