JWT:
生成JWT(加密)
可以看到登录成功之后,调用了jwtUtils.generateToken(User.getId())根据userId生成一个完整的jwt字符串,并把该jwt字符串放在了响应头中 有个疑问,客户端收到响应头以后会把这个jwt放在cookie中吗?-->会,但是并不是以后每次发起请求都会携带jwt发出请求,携带与否是前端决定。
解析JWT(解密)
从请求头获取到jwt以后,调用jwtUtils.getClaimByToken(jwt)对加密过的jwt进行解密,解密得到Claims对象,
在加密过程中,我们把userId放在了Claims对象的SUBJECT属性中
Shiro逻辑分析:
对每一次请求都进行过滤,分为携带jwt与不携带jwt的情况
如果不携带jwt的话过滤器直接放行,如果携带jwt的话进入Shiro的登录认证
重写createToken(request,response)方法
自定义Realm,实现doGetAuthenticationInfo()与doGetAuthorizationInfo()
由于响应要封装为统一结果类Result,所以重写onLoginFailure()
关于shiro-redis在哪里使用了Redis实现分布式会话共享?
DefaultSessionManger中的SessionDAO用于session persistence,如果不设置,默认的是MemeorySessionDAO(ConcurrentHashMap),用内存持久化session(无法解决分布式服务器会话共享)
而在这个项目中,使用的是用redis持久化session
subject.login(token):
token是用户提交的东西,一般有两部分Principal(账号或者用户名)和Credentials(密码或者验证的东西)。用户提交了token,则会进入自定义的Realm的doGetAuthenticationInfo()中获取我们返回的SimpleAuthenticationInfo。此时token包含principal与credentials,simpleAuthenticationInfo同样包含principal与credentials,区别就在于:token中的principal与credentials由用户决定,simpleAuthentication中的principal是由业务决定,最关键的credentials是根据token中的principal得到(可能是查session也可能是查数据库)。之后shiro便开始校验:校验用户给出的token中的credentials是否与simpleAuthentication中的credentials一致。
至于分布式会话共享:
需求在于:当用户已经获取到jwt,并且已经登录成功一次的时候,要通过session记录其已登陆状态。当再次发起请求的时候,不需要再次登录了,因为session中已有他的登录记录。但由于分布式服务器的存在,每次用户的请求可能不会委派给同一台服务器,便需要会话共享。多台服务器之间共享统一session记录,redis可以实现这点。每次用户登陆成功以后,shiro创建session,并生成JSESSIONID与session一一对应,一方面将JSESSIONID-session存入redis,一方面将JESSIONID存入cookie返回给客户端。由此,当用户再次发起请求并携带cookie的时候,shiro会根据cookie中的JESSIONID查询redis是否有该key,有的话则直接不需要再登录,没有的话就进行传统doGetAuthenticationInfo()。部署redis的服务器要允许部署项目的服务器访问redis,可以这些服务器通内网(最安全),也可以部署redis的服务器设置bind为0.0.0.0(暴露在公网上),并设置redis密码,同时可以配置安全组。
关于JSESSIONID:
Shiro各个配置:
1.自定义Realm:(关于角色与权限后期再开发,先实现认证功能)
注意:Shiro中使用自定义token时,要重写下面方法:
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
完整代码:
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 授权(认证成功后授予权限)
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证(认证帐号密码)
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
JwtToken jwtToken=(JwtToken) authenticationToken;
String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();//shiro收到的是token,token携带jwt,解析得到id
User user = userService.getById(Long.valueOf(userId));
if(user==null){
throw new UnknownAccountException("账户不存在");
}
if(user.getStatus()==-1){
throw new LockedAccountException("账户已被锁定");
}
AccountProfile profile=new AccountProfile();
BeanUtils.copyProperties(user,profile);
return new SimpleAuthenticationInfo(profile,jwtToken.getCredentials(),getName());
}
}
2.自定义JwtFilter
实现createToken():在请求经过过滤器的时候,生成token并返回
实现onAccessDenied():处理放行还是拦截的逻辑
重写onLoginFailure():为了返回给前端统一结果类Result
实现preHandle():对跨域提供支持
自定义Token:principal与credential都是token本身
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String jwt){
this.token=jwt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
完整代码:
@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
/**
* 生成token
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request=(HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if(jwt==null||jwt.length()==0){//没有获取到jwt
return null;
}
return new JwtToken(jwt);
}
/**
* 拦截,判断有无jwt
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request=(HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if(jwt==null||jwt.length()==0){
return true;//放行,比如游客,就不携带jwt
}else{
//校验jwt
Claims claim = jwtUtils.getClaimByToken(jwt);
if(claim==null || jwtUtils.isTokenExpired(claim.getExpiration())){ //null表示出异常,expired表示已过期
throw new ExpiredCredentialsException("token已失效,请重新登录");
}
//执行登录
return executeLogin(servletRequest,servletResponse);
}
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpServletResponse=(HttpServletResponse)response;
Throwable throwable= e.getCause()==null?e:e.getCause();
Result result = Result.fail(throwable.getMessage());
String json = JSONUtils.toJSONString(result);//因为是过滤器返回结果,不是Controller里有@ResponseBody注解,所以需要手动转json并利用输出流返回给前端
try {
httpServletResponse.getWriter().write(json); //TODO httpServletResponse.getWriter().print(json)与write有什么区别
} catch (IOException ex) {
ex.printStackTrace();
}
return false;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
3. ShiroConfig
配置RedisManager:配置redis
配置SessionManager: 配置Shiro的session持久化
配置SessionSecuiryManager:配置shiro的自定义认证逻辑;Cache的作用是把每次通过Realm查询到的结果存入Session,以便用户访问时加快Authentication和Authorization。如果要使用cache,必须在 securityManager 和 sessionManager 中同时进行配置。(所以cacheManager作用是生成Session?sessionManager的作用是将生成的Session持久化到redis中?)
配置ShiroFilterFactoryBean: 添加自定义过滤器并取名字;添加拦截链到过滤器工厂。
配置ShiroFilterChainDefinition: 配置拦截链(各个过滤器所负责过滤的请求路径)
@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database}")
private int database;
/**
* redis的控制器,控制shiro-redis-spring-boot-starter连接的redis,默认是localhost:6379
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setDatabase(database);
redisManager.setPassword(password);
return redisManager;
}
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// inject redisSessionDAO
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
@Bean
public SessionsSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
//inject sessionManager
securityManager.setSessionManager(sessionManager);
// inject redisCacheManager
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限 //filterMap.put("/**", "authc"); authc是默认的,所有请求都需要登陆后才能访问
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition,
JwtFilter jwtFilter) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
shiroFilter.setFilters(filters);
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();//此时所有的请求都会经过jwtFilter
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}