讲认证之前先提以下注册,毕竟有用户才能认证。
com.demo.controller.register.RegisterController 的 userRegister方法:
/*
* Author : baiye
Time : 2021/06/30
Function:
*/
@RestController
@RequestMapping("/register/")
public class RegisterController {
@Autowired
IUserService iUserService;
@RequestMapping(value = "register.do", method = RequestMethod.POST)
@ResponseBody
public ServerResponse userRegister(@RequestBody UserDTO userDto) {
//TODO userid 不能重复check
return iUserService.userRegister(userDto);
}
}
com.demo.service.impl.UserServiceImpl的userRegister方法:
这里记住密码加密盐值使用的是userId:用userId作为盐值去加密用户输入的password,
使用的是shiro的SimpleHash,加密算法MD5(觉得MD5不安全可以换别的加密算法)。
/**
* 用户注册
* @param userDto
* @return
*/
@Transactional
public ServerResponse userRegister(UserDTO userDto) {
// 用户名
String userId = userDto.getUserId();
// 用户密码
String passWord = userDto.getPassWord();
// MD5密码加密 salt为userId
String md5PassWord = ShiroMD5Util.encryptPassword(passWord, userId);
// 用户主表entity
UserMaster userMaster = new UserMaster();
// 属性copy
BeanCopier copier = BeanCopier.create(UserDTO.class, UserMaster.class, false);
copier.copy(userDto, userMaster, null);
// 加密后的密码
userMaster.setPassWord(md5PassWord);
// 删除FLG : 0
userMaster.setDelFlg(ConstCode.STRING_ZERO);
Date date = new Date();
userMaster.setRegAccount(userId);
userMaster.setRegTime(date);
userMaster.setUpdAccount(userId);
userMaster.setUpdTime(date);
userMasterMapper.insert(userMaster);
// 用户注册成功
return ServerResponse.createBySuccessMessage("用户注册成功");
}
com.demo.util. ShiroMD5Util 的 encryptPassword方法:
/**
* 使用MD5加密
*
* @param password 需要加密的密码
* @param salt 加密的盐值 => 用户名
* @return 返回加密后的密码
*/
public static String encryptPassword(String password, String salt) {
// MD5 哈希算法
// salt 作为盐值
// 666 hash迭代次数
return String.valueOf(new SimpleHash("MD5", password, salt, 666));
}
这里还需要注意要将注册路径加入ShiroFilterFactoryBean(com.demo.config.ShiroConfig)的FilterChainDefinitionMap中,设置成anon:无需认证可以访问,如下:
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filterMap = new LinkedHashMap();
// 只有不需要权限认证(anon)的 需要明确写入filterMap
filterMap.put("/hello", "anon");
filterMap.put("/register/*", "anon");
filterMap.put("/login/*", "anon");
// 添加自定义过滤器并且取名为jwt
Map filter = new HashMap(1);
filter.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filter);
// 过滤链定义,从上向下顺序执行,所以放在最为下边
filterMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
测试使用postman,参数格式用Json,注册成功返回status 200 msg 用户注册成功,如下:
com.demo.controller.login.LoginController的login方法
/**
* /login/login.do 用户登录
* @param userLoginDTO
* @param request
* @param response
* @return
* @throws Exception
*/
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
@ResponseBody
public ServerResponse login(@RequestBody LoginDTO userLoginDTO,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
return iLoginService.login(userLoginDTO, request, response);
}
com.demo.service.impl.LoginServiceImpl的login方法,说明基本都写在注释里了。
首先是创建CustomizedToken,并且指明使用BY_PASSWORD方式进行认证,
然后主动调用subject.login进行认证。
/**
* 用户login
*
* @param loginDTO
* @param request
* @param response
* @return
*/
@Override
public ServerResponse login(LoginDTO loginDTO, HttpServletRequest request, HttpServletResponse response) {
// 用户ID
String userId = loginDTO.getUserId();
// 密码
String passWord = loginDTO.getPassWord();
// 获取Subject
Subject subject = SecurityUtils.getSubject();
// 校验数据库中此user是否存在
UserMaster userMaster = userMasterMapper.selectByUserId(userId);
if (userMaster == null) {
// 该用户未注册 status 602
return ServerResponse.createByBizError("该用户未注册");
}
userId = userMaster.getUserId();
// 制作CustomizedToken执行登录(shiro UsernamePasswordToken)
CustomizedToken customizedToken = new CustomizedToken(userId, passWord, LoginEnum.BY_PASSWORD.getLoginType());
// 记住密码
//customizedToken.setRememberMe(userLoginDTO.getRememberMe());
try {
// shiro 用户认证
subject.login(customizedToken);
} catch (Exception e) {
log.error(e.getMessage());
// 用户认证失败 密码错误 status 400
return ServerResponse.createByErrorMessage("用户认证失败,用户名/密码错误。");
}
String sessionId = String.valueOf(subject.getSession().getId());
log.debug("登录用户 : {}, j_session_id : {}", userId, sessionId);
// 清除可能存在的shiro权限信息缓存
if (redisUtil.hasKey(RedisConst.PREFIX_SHIRO_CACHE + userId)) {
redisUtil.del(RedisConst.PREFIX_SHIRO_CACHE + userId);
}
// RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken)
String currentTimeMillis = String.valueOf(SysTimeUtil.getTime());
/**
* 将发给用户认证的access token保存到redis中,之后请求时header携带token,
* ★★需要重写shiro的sessionManager去redis中获取该token作为sessionid来进行认证★★
*/
// redis 存存储refresh Token信息, key shiro:refresh_token:userid 当前时间 过期时间一天
redisUtil.set(RedisConst.PREFIX_SHIRO_REFRESH_TOKEN + userId, currentTimeMillis,
Const.REFRESH_TOKEN_EXPIRE_TIME);
// 获取用户详细信息
UserInfo userInfo = userMasterMapper.getUserInfo(userId);
String accountType = userInfo.getAccountType();
// 发给用户认证的access token 之后请求时header携带token,过期时间 3小时
String token = JwtUtil.loginSign(userId, accountType, currentTimeMillis, sessionId, Const.TOKEN_SECRET);
// Authorization
response.setHeader(Const.TOKEN_HEADER_NAME, token);
// Access-Control-Expose-Headers : Authorization
response.setHeader(Const.TOKEN_ACCESS_CONTROL, Const.TOKEN_HEADER_NAME);
// 返回 SimpleUserInfo token, UserInfo
return ServerResponse.createBySuccess(new SimpleUserInfo(token, userInfo));
}
调用subject.login之后,由于已经将UserModularRealmAuthenticator注入SecurityManager,所以会先执行UserModularRealmAuthenticator的doAuthenticate方法。
又由于subject.login传递的是BY_PASSWORD的CustomizedToken,所以接下来将进入到PasswordRealm进行认证doGetAuthenticationInfo。
/*
* Author : baiye
Time : 2021/06/30
Function:
*/
@Slf4j
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
// 当subject.login()方法执行,下面的代码即将执行
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
log.debug("UserModularRealmAuthenticator:method doAuthenticate() 执行 ");
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 所有Realm
Collection realms = getRealms();
// 盛放登录类型对应的所有Realm集合
Collection typeRealms = new ArrayList<>();
if(authenticationToken instanceof JwtToken) {
log.debug("验证的Token类型是:{}", "JwtToken");
typeRealms.clear();
// 获取header部的token进行强制类型转换
JwtToken jwtToken = (JwtToken) authenticationToken;
for (Realm realm : realms) {
if (realm.getName().contains("Demo")) {
typeRealms.add(realm);
}
}
return doSingleRealmAuthentication(typeRealms.iterator().next(), jwtToken);
} else {
typeRealms.clear();
// 这个类型转换的警告不需要再关注 如果token错误 后面将会抛出异常信息
CustomizedToken customizedToken = (CustomizedToken) authenticationToken;
log.debug("验证的Token类型是:{}", "CustomizedToken");
// 登录类型
String loginType = customizedToken.getLoginType();
log.debug("验证的realm类型是:{}", loginType);
for (Realm realm : realms) {
if (realm.getName().contains(loginType)) {
log.debug("当前realm:{}被注入:", realm.getName());
typeRealms.add(realm);
}
}
// 判断是单Realm还是多Realm
if (typeRealms.size() == ConstCode.NUM_1) {
log.debug("一个realm");
return doSingleRealmAuthentication(typeRealms.iterator().next(), customizedToken);
} else {
log.debug("多个realm");
return doMultiRealmAuthentication(typeRealms, customizedToken);
}
}
}
}
PasswordRealm中只做认证,因为后续通过token访问,授权放到其他的Realm去实现。
这里注意返回的SimpleAuthenticationInfo传参,说明写在注释里面了。
/*
* Author : baiye
Time : 2021/06/30
Function: 用户密码认证Realm
*/
@Slf4j
public class PasswordRealm extends AuthorizingRealm {
@Autowired
@Lazy
private IUserService iUserService;
/**
* shiro 赋权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.debug("PasswordRealm 不做赋权处理");
// password login 不做赋权处理
return null;
}
/**
* shiro 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
log.debug("PasswordRealm权限认证开始,传递的token:{}", authenticationToken);
CustomizedToken token = (CustomizedToken) authenticationToken;
log.debug("PasswordRealm转换的自定义token:{}", token);
// 找出数据库中的对象 和用户输入的对象做出对比
// 根据userid查询用户
UserMaster userMaster = iUserService.getUserByUserId(token.getUsername());
if (userMaster == null) {
log.debug("该账号不存在:{}", token.getUsername());
// 抛出账号不存在异常
throw new UnknownAccountException();
}
// 用户密码
Object hashedCredentials = userMaster.getPassWord();
/***
* param1 : 数据库用户
* param2 : 密码
* param3 : 加密所用盐值
* param4 : 当前realm的名称
*/
return new SimpleAuthenticationInfo(userMaster, hashedCredentials,
ByteSource.Util.bytes(userMaster.getUserId()), getName());
}
}
subject.login(customizedToken)成功后,获取到认证成功的sessionId,并生成access_token,access_token里面携带sessionId,当前时间戳以及用户ID等信息。并同时往redis存入useid对应的access_token的时间戳,保存为refresh_token,保证后续可以验证刷新access_token。subject.login已经贴过全部service login方法的全部代码了,这里就贴以下关键代码了。
// shiro认证通过后的sessionID
String sessionId = String.valueOf(subject.getSession().getId());
// redis 存存储refresh Token信息, key shiro:refresh_token:userid 当前时间 过期时间一天
redisUtil.set(RedisConst.PREFIX_SHIRO_REFRESH_TOKEN + userId, currentTimeMillis,
Const.REFRESH_TOKEN_EXPIRE_TIME);
// 发给用户认证的access token 之后请求时header携带token,过期时间 3小时
String token = JwtUtil.loginSign(userId, accountType, currentTimeMillis, sessionId, Const.TOKEN_SECRET);
login.do接口也同样再ShiroConfig里设置成anon:无需认证可以访问。
这里就使用之前注册的demo006:
解析一下生成的access_token,信息如下:
最后再确认一下redis中保存的refresh_token的value是否与access_token的时间戳一致。
密码错误的情况
至此,认证功能就大体完成了。