前后端分离(分布式项目中)
理解:
1、shiro的登录流程
1 得到一个安全管理器SecurityUtils
2 在安全管理器里得到一个Subject
3 在主体里面调用subject.login(token) 该token中包含了用户名和用户密码
4 调用login 方法,shiro 底层会调用Realm 里面的认证方法
5 认证方法里面,会使用用户名称去查询用户
6 查询成功后,会把用户的名称和密码 return
7 shiro 底层会验证用户和密码
8 若shiro 验证成功,则会把用户存储在自己session 里面,其中key 是:sessionId,value是登录的用户对象
2、session的理解
如果是前后端不分离项目,就直接从cookie中获取jsessionid,通过jsessionid找到存储用户信息的session。
如果是前后端分离项目,由于前端服务器和后端服务器不是一个,所以jessionid不能放在cookie中。这时,就可以将cookie中获取的jessionid放在请求头中(安全)。但是后端项目模块较多,由于从请求头中通过jessionid获取的session不能在多个tomcat中共享,所以就需要使用redis进行存储,以实现session的共享。
3、使用shiro提供的接口
3.1、DefaultWebSessionManager接口
DefaultWebSessionManager接口从请求头中获取token,如果没有则生成一个token。
JSessionid 只有一个标识而已。可以找个别的代替它!
Shiro 给我们提供了DefaultWebSessionManager接口,实现他就可以很轻易的把JessionId 换成别的数据
3.2、SessionDAO接口(实现了shiro的session共享)
SessionDAO接口将获取的token进行存储。
Shiro的底层有个SessionDAO 的接口,该接口负责从某个地方读取Session,或写入session
4、百度案例
利用请求头解决session共享问题。
token概要
概要:Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。
代码实现
引入依赖
<!--shiro 和spring boot web 集成的方案-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
</dependency>
<!-- redis 和shiro 整合 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
</dependency>
<!--用处:存储验证码,用户权限等-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件
# 该app端口的配置
server:
port: 8085
# 数据源配置
spring:
datasource:
url: jdbc:mysql://47.94.225.69:3306/ego-shop
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# redis的配置
redis:
port: 6379
host: 47.94.225.69
# mybatis-plus的配置
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.zxm.entity
configuration: # 日志的打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
登录对象的封装类
使用validator做了后端的表单校验
package com.zxm.param;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 构造登陆的请求参数
*/
@Data
public class LoginParam {
@NotBlank(message = "登录用户名不能为null")
@ApiModelProperty("用户名")
private String principal;
@NotBlank(message = "登录密码不能为null")
@ApiModelProperty("密码")
private String credentials;
@NotBlank(message = "自动生成的uuid不能为空")
@ApiModelProperty("前端生成的uuid")
private String sessionUUID;
@NotBlank(message = "用户填写的验证码不能为空")
@ApiModelProperty("用户填写的验证码")
private String imageCode;
}
LoginController类
登录的整体流程。使用了redis存储验证码
package com.zxm.controller;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.captcha.LineCaptcha;
import com.zxm.param.LoginParam;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.CredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.pam.UnsupportedTokenException;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* 前后端分离开发里面,登陆成功后,后端不需要进行跳转。
*/
@ApiOperation(value = "登录的数据接口")
@RestController // 相当于@RequestBody+@RequestMapping
public class LoginController {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String VALIDATE_CODE_PREFIX = "validate:";
/**
* 补全shiro自动配置类里面的两个路径
*/
@GetMapping("/login")
public ResponseEntity<String> toLogin() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户还没有登录,请先登录!");
}
@GetMapping("/unauthorizedUrl")
public ResponseEntity<String> unauthorizedUrl() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("该用户权限不够,请联系管理员提升权限!");
}
/**
* 完成shiro的登录功能
*
* @RequestParam表示请求参数不能为空。
*/
@ApiOperation("登录")
@PostMapping("/login")
public ResponseEntity<String> doLogin(
@RequestBody @Valid LoginParam loginParam
) {
try {
checkValidateCode(loginParam);
UsernamePasswordToken token = new UsernamePasswordToken(loginParam.getPrincipal(), loginParam.getCredentials());
Subject subject = SecurityUtils.getSubject();
subject.login(token);
// 登录成了,它把用户的登录信息写在sessions 里面,而sessions 里面key时SessionId
String sessionId = subject.getSession().getId().toString();
return ResponseEntity.ok(sessionId);
} catch (AccountException e) { // 用户不存在
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("用户名不存在,或用户名错误");
} catch (CredentialsException credentialsException) { // 密码错误
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("密码过期或错误");
} catch (UnsupportedTokenException unsupportedTokenException) { // token 不支持
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("TOKEN 不被支持");
} catch (AuthenticationException validateCodeException) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(validateCodeException.getMessage());
} catch (RuntimeException runtimeException) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("服务器错误");
}
}
@ApiOperation(value = "获取验证码")
@SneakyThrows // 自动关闭资源
@GetMapping("/captcha.jpg")
public ResponseEntity<Void> getCaptcha(
@RequestParam(required = true) String uuid,
HttpServletResponse response
) {
// 使用hutool工具类生成一个验证码图片
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 50, 4, 3);
String validateCode = lineCaptcha.getCode();// 获取验证码,将其存储在redis中。
redisTemplate.opsForValue().set(VALIDATE_CODE_PREFIX + uuid, validateCode, 1, TimeUnit.MINUTES);
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
lineCaptcha.write(outputStream);
} catch (IOException e) {
e.printStackTrace();
}
return ResponseEntity.ok().build();
}
/**
* 校验用户的验证码
*/
private void checkValidateCode(LoginParam loginParam) {
String sessionUUID = loginParam.getSessionUUID();
String imageCode = loginParam.getImageCode();
if (StringUtils.hasText(sessionUUID) && StringUtils.hasText(imageCode)) {
String code = redisTemplate.opsForValue().get(VALIDATE_CODE_PREFIX + sessionUUID);
if (imageCode.equals(code)) {
redisTemplate.delete(VALIDATE_CODE_PREFIX + sessionUUID);
return;
}
}
throw new AuthenticationException("验证码错误,请重新获取!");
}
@ApiOperation(value = "登出")
@PostMapping("/sys/logout")
public ResponseEntity<Void> logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return ResponseEntity.ok().build();
}
}
UserRelam类
实现用户的认证和授权
package com.zxm.shiro;
import com.zxm.entity.SysUser;
import com.zxm.service.SysUserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@Component
public class UserRealm extends AuthorizingRealm {
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 登录认证只进行一次
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
/**
* 用户的信息保存在token中,由subject.login(token)方法传递过来的。
*/
String username = token.getPrincipal().toString();
// 根据用户名查询用户信息
SysUser sysUser = sysUserService.queryUserByUserName(username);
if(sysUser==null){
return null; // shiro 会处理null的情况,底层会抛一个用户不存在的异常
}
// 屏蔽用户的私人信息后,把用户给它
sysUser.setEmail("*******");
ByteSource byteSource = ByteSource.Util.bytes("zxm"); // 盐可以写死,也可以从数据库中获取。sysUser.getSalt();
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(sysUser, sysUser.getPassword(), byteSource, sysUser.getUsername());
return simpleAuthenticationInfo;
}
/**没有url地址 但是需要权限访问时,都会访问该方法。
* 做授权
* 授权可能运行多次
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUser sysUser = (SysUser) principals.getPrimaryPrincipal();
Set<String> perms = (Set<String>) redisTemplate.opsForValue().get("AUTH_PERMS:" + sysUser.getUserId());
// simpleAuthorizationInfo.setRoles();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(perms);
return simpleAuthorizationInfo;
}
}
TokenSessionManager类
获取请求头中的token,如果请求头中没有token,则重新生成一个token
package com.zxm.shiro;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
import java.util.UUID;
/**
* 登陆成功的标识,每次访问资源都会携带这个标识。
*/
@Component
public class TokenSessionManager extends DefaultWebSessionManager {
private static final String AUTH_HEADER = "EGO_TOKEN";
/**
* shiro 默认的行为,是从request 里面取出tomcat 的sessionId 来做为 key
* @param request
* @param response
* @return
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
/**
* app的token从数据库中取到存储到header中。
* pc端token从cookie中取到存储到header中。
* 不管是app还是pc端,都将标识存储在头中,使用头可以屏蔽差异。
*/
String token = WebUtils.toHttp(request).getHeader(AUTH_HEADER);
if(StringUtils.hasText(token)){ // 若请求中携带token,就直接返回token,否则生成一个token。
return token;
}
token = UUID.randomUUID().toString();
return token;
}
}
ShiroCustomConfiguration类
shiro的配置类。
package com.zxm.config;
import com.zxm.shiro.TokenSessionManager;
import com.zxm.shiro.UserRealm;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.IRedisManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.sound.sampled.Port;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义shiro的配置
*/
@Configuration
public class ShiroCustomConfiguration {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private Integer redisPort;
/**
* 安全管理器
* @return
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(
UserRealm userRealm, // realm负责登陆和授权
TokenSessionManager tokenSessionManager, // 负责token的管理
RedisSessionDAO sessionDAO, // SessionDAO负责token的存储 使用其子类RedisSessionDAO也行。
CredentialsMatcher credentialsMatcher // 负责密码的校验
){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
userRealm.setCredentialsMatcher(credentialsMatcher);
defaultWebSecurityManager.setRealm(userRealm); // 自定义realm
tokenSessionManager.setSessionDAO(sessionDAO);// session怎么存储由sessionDAO决定
defaultWebSecurityManager.setSessionManager(tokenSessionManager);
return defaultWebSecurityManager;
}
/**
* 使用redis存储session的具体实现类
* @param redisManager
* @return
*/
@Bean
public RedisSessionDAO redisSessionDAO(IRedisManager redisManager){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager); // 去存储在哪个redis里面
redisSessionDAO.setExpire(7*24*3600); // 设置session的过期时间
// redisSessionDAO.setKeyPrefix(); // 设置存储在redis里面的前缀,默认是redis:session:
return redisSessionDAO;
}
/**
* 决定使用哪个redis
*
* @return
*/
@Bean
public IRedisManager iRedisManager(){
RedisManager redisManager = new RedisManager();
if(StringUtils.hasText(redisHost)){
if(redisPort != null){
redisManager.setHost(redisHost+":"+redisPort);
}else{
redisManager.setHost(redisHost+":"+6379);
}
}
return redisManager;
}
/**
* 密码的验证器
* @return
*/
@Bean
public CredentialsMatcher credentialsMatcher(){
// 底层使用MD5加密
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher("MD5");
// 散列2次
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 资源放行管理
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 用户没有登录访问某个url地址时,shiro会把用户重定向到这个login页面
shiroFilterFactoryBean.setLoginUrl("login");
// 用户登录成功访问没有授权的页面时,会重定向到这个unauthorizedUrl页面
shiroFilterFactoryBean.setUnauthorizedUrl("unauthorizedUrl");
// 登录成功后,跳转到那个页面里面,在前后端分离开发里面,不使用,因为前端的跳转,由前端自己的js 去完成
// shiroFilterFactoryBean.setSuccessUrl("");
Map<String, String> filterChainDefinitionMap = new HashMap<>();
filterChainDefinitionMap.put("/unauthorizedUrl","anon") ;
filterChainDefinitionMap.put("/login","anon"); // 放行的资源
filterChainDefinitionMap.put("/captcha.jpg","anon"); // 放行验证码
filterChainDefinitionMap.put("/**","authc"); // 拦截的资源
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}
/**
* 加入注解的使用。例如使用注解监控用户重要操作。
* 不加入这个则注解不生效!
* Spring Boot start Aop = Spring aop + aspectjweaver
* DefaultAdvisorAutoProxyCreator是spring aop里面的一个类
*
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
/**
* true: 使用Cglib的代理方式
* false: 使用jdk的代码方式
* 这个东西是自己new
* 在shiro 也有该对象的创建形式,但是shiro 里面默认为false ,为jdk ,当为jdk ,以及集成spring boot aop 后,controller 就失效了
* 注意:shiro也有自己对注解的切面。默认使用jdk代理方式,如果想使用aop的切面注解,则必须使用cglib的动态代理。
*/
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);// 使用cgilb 给Shiro 创建一个代理对象,而不是JDK
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}