这里是搞前端的菜鸡一个,在公司实习的时候组长给派了个任务,让实现系统的登录认证功能。好在以前做课设的时候有做过相关的功能,所以也不算是为难我。根据我本人的经验选了jwt+shiro+redis来实现登录认证的功能。顺带一提前端是react+umi+dva.
数据库的设计是考虑到RBAC的用户-角色-权限方式来设计的,虽然这里主要是做登录认证的功能,但是后续还想做权限管理的功能,所以表还是要按规矩设计。主要涉及到以下几个表。
在这里按照前端发起一个请求,然后shiro的拦截器拦截到请求,返回响应的一个过程来依次列出涉及到的文件。因为我自己就是摸索这个过程花费了比较多的时间。卑微前端踩坑太难了QAQ
首先在pom.xml中引入shiro,其他基本的springboot的依赖就不多说啦。
org.apache.shiro
shiro-spring
1.3.2
/**
* shiro配置文件
*/
@Configuration
public class ShiroConfiguration {
private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);
//从配置文件里面读取是否需要启动登录认证的开关,默认true
@Value("${jwt.auth}")
private boolean auth;
//配置拦截器
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//启用认证
String openAuth = auth ? "auth" : "anon";
//自定义过滤器链
Map<String, Filter> filters = new HashMap<>();
//指定拦截器处理
filters.put("auth", new AuthFilter());
shiroFilterFactoryBean.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
//登录请求不拦截
filterMap.put("/user/login", "anon");
//登录页面需要用到的接口,不拦截
filterMap.put("/user/fetchCurrentUser", "anon");
//拦截所有接口请求,做权限判断
filterMap.put("/**", openAuth);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
logger.info("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
// SecurityManager 安全管理器;Shiro的核心
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
return securityManager;
}
//自定义身份认证realm
@Bean
public AuthRealm userRealm() {
return new AuthRealm();
}
@Bean("lifecycleBeanPostProcessor")
//管理shiro生命周期
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
//Shiro注解支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
auth
变量是因为大家正在开发过程中,每每请求认证会导致不便,要求给一个能够控制是否认证的开关放在配置文件,一般就是application.yml或者application.properties,我随便起了个名字叫jwt.auth
,直接放在配置文件里面就好了,然后使用@Value
注解去配置文件里读。lifecycleBeanPostProcessor()
方法定义为了静态的,这里是因为要去配置文件里面读取auth变量,如果不定义为静态的话会出现因为springboot扫描bean的顺序先后而导致这个文件里读取不到auth的值的问题,所以改成static了来绕过这个问题。filters.put("auth", new AuthFilter());
这一句就是给认证标签auth指定了一个拦截器,我们的主要功能就是在这个拦截器里面实现。/**
* 实现自定义的认证拦截器,接收传过来的token,实现前后端分离的权限认证
*/
public class AuthFilter extends AuthenticatingFilter {
private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
private Result responseResult = ResultUtils.forbiddenError(AuthConstant.AUTHENTICATE_FAIL);
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
return null;
}
/**
* 在这里拦截所有请求
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String token = JwtAuthenticator.getRequestToken((HttpServletRequest)request);
if (!StringUtils.isBlank(token)){
try {
this.executeLogin(request, response);
} catch (Exception e) {
// 应用异常
logger.info(e.getMessage());
responseResult = ResultUtils.forbiddenError(e.getMessage());
return false;
}
} else {
// cookie中未检查到token或token为空
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
String httpMethod = httpServletRequest.getMethod();
String requestURI = httpServletRequest.getRequestURI();
responseResult = ResultUtils.forbiddenError(AuthConstant.TOKEN_BLANK);
logger.info("请求 {} 的Token为空 请求类型 {}", requestURI, httpMethod);
return false;
}
return true;
}
/**
* 请求失败拦截,请求终止,不进行转发直接返回客户端拦截结果
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception{
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
httpServletResponse.setContentType("application/json; charset=utf-8");
httpServletResponse.setCharacterEncoding("UTF-8");
String result = JsonConvertUtil.objectToJson(responseResult);
httpServletResponse.getWriter().print(result);
return false;
}
/**
* 用户存在,执行登录认证
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
String token = JwtAuthenticator.getRequestToken((HttpServletRequest)request);
AuthTokenVo jwtToken = new AuthTokenVo(token);
// 提交给AuthRealm进行登录认证
getSubject(request, response).login(jwtToken);
return true;
}
}
isAccessAllowed()
方法,进行初步判断,首先拿到浏览器cookie中的token,进行判空,若不为空执行下一步executeLogin()
来执行真正的登录token认证。isAccessAllowed()
返回结果为true就说明认证通过了,前端可以尽情请求资源啦。出现任何错误都会返回false,就会跳到onAccessDenied()
方法,处理拦截结果返回给前端。/**
* 自定义安全数据Realm
*/
public class AuthRealm extends AuthorizingRealm {
private static final transient Logger logger = LoggerFactory.getLogger(AuthRealm.class);
@Autowired
private UserService userService;
/**
* 重写,绕过身份令牌异常导致的shiro报错
* @param authenticationToken
* @return
*/
@Override
public boolean supports(AuthenticationToken authenticationToken){
return authenticationToken instanceof AuthTokenVo;
}
/**
* 执行授权逻辑
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
logger.info("用户角色权限认证");
//获取用户登录信息
UserVo userVo = (UserVo)principals.getPrimaryPrincipal();
//添加角色和权限
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
for(RoleVo role : userVo.getRoleVoList()){
authorizationInfo.addRole(role.getRoleName());
for(PermissionVo permissionVo : role.getPermissionVoList()){
authorizationInfo.addStringPermission(permissionVo.getPermissionName());
}
}
return authorizationInfo;
}
/**
* 执行认证逻辑
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
logger.info("执行认证逻辑");
//获得token
String token = (String)authenticationToken.getCredentials();
//获得token中的用户信息
String user = JwtAuthenticator.getUsername(token);
//判空
if(StringUtils.isBlank(user)){
throw new AuthenticationException(AuthConstant.TOKEN_BLANK);
}
try{
//查询用户是否存在
List<UserVo> userVo = userService.login(new UserQuery(user, null));
if(userVo.size() <= 0){
throw new AuthenticationException(AuthConstant.TOKEN_INVALID);
//token过期
}else if(!(JwtAuthenticator.verifyToken(token, user, userVo.get(0).getLoginPWD()))){
throw new AuthenticationException(AuthConstant.TOKEN_EXPIRE);
}
}catch (Exception e){
throw e;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
token, token, "auth_realm");
return authenticationInfo;
}
}
AuthorizingRealm.java
的两个方法。登录认证这一块的功能是doGetAuthenticationInfo(AuthenticationToken authenticationToken)
来实现。AuthFilter.java
进行相应的处理。直到返回正确的认证信息,说明认证成功。/**
* 权限相关的常量
*/
public class AuthConstant {
/**
* cookie中存储的token字段名
*/
public final static String COOKIE_TOKEN_NAME = "Authorization";
/**
* token有效时间 时*分*秒*1000L
*/
public final static Long EXPIRE_TIME = 3*60*1000L;//先设置3分钟
//登录认证结果,返回给前端
public final static String UNKNOWN_ACCOUNT = "登录失败, 用户不存在。";
public final static String WRONG_PASSWORD = "登录失败,密码错误。";
public final static String TOKEN_BLANK = "验证失败,token为空,请登录。";
public final static String TOKEN_INVALID = "验证失败,token错误。";
public final static String TOKEN_EXPIRE = "验证失败,token过期,请重新登录。";
public final static String AUTHENTICATE_FAIL = "无访问权限,请尝试登录或联系管理员。";
}
认证链中用到的常量。
在pom.xml中引入jwt的依赖
com.auth0
java-jwt
3.7.0
写一个jwt的工具类来做token的效验
public class JwtAuthenticator {
/**
* 校验token是否正确
* @param token
* @param username
* @param secret
* @return
*/
public static boolean verifyToken(String token, String username, String secret){
// 根据密码生成JWT校验器
try{
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username",username)
.build();
// 校验token,这里是jwt的内部实现,可能会抛出错误(token错误或过期等),同样将错误抛回给上层
DecodedJWT jwt = verifier.verify(token);
return true;
}catch (Exception e){
return false;
}
}
/**
* 获得token中的用户信息,无需解密
* @param token
* @return
*/
public static String getUsername(String token){
try{
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
}catch (JWTDecodeException e){
return null;
}
}
/**
* 生成签名
* @return
*/
public static String sign(UserVo userVo, Date expireTime) {
String secret = userVo.getLoginPWD();
String userName = userVo.getUserName();
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息和过期信息
return JWT.create()
.withClaim("username", userName)
.withExpiresAt(expireTime)
.sign(algorithm);
}
/**
* 从cookie中获取token
* @param httpServletRequest
* @return
*/
public static String getRequestToken(HttpServletRequest httpServletRequest){
String token = "";
Cookie[] cookies = httpServletRequest.getCookies();
if(cookies != null){
for(Cookie ck : cookies){
if(StringUtils.equals(AuthConstant.COOKIE_TOKEN_NAME, ck.getName())){
token = ck.getValue();
break;
}
}
}
return token;
}
/**
* 编辑浏览器cookie
* @param response
* @param tokenValue
*/
public static void editCookieToken(ServletResponse response, String tokenValue){
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
Cookie cookie = new Cookie(AuthConstant.COOKIE_TOKEN_NAME, tokenValue);
cookie.setPath("/");
cookie.setHttpOnly(true);//前端不可读cookie
//跨域向前端写cookie
httpServletResponse.setHeader("Access-Control-Allow-Origin",
httpServletResponse.getHeader("Origin"));
httpServletResponse.addCookie(cookie);
}
}
verifyToken(String token, String username, String secret)
是实现验证token的主要方法,token会是一串很长的码然后jwt会自己从里面读取出用户信息,并验证token是否有效。sign(UserVo userVo, Date expireTime)
方法是实现在用户登录的时候,根据用户信息和我们规定的过期时间来生成一个独一无二的签名(token),我们会将这个token返回给前端,作为用户在前端访问后台接口的唯一凭证。public class AuthTokenVo implements AuthenticationToken {
private String token;
public AuthTokenVo(String token){
this.token = token;
}
@Override
public Object getPrincipal(){
return token;
}
@Override
public Object getCredentials(){
return token;
}
}
这个是token的实体类,需要实现AuthenticationToken接口。
用户登录就是普通的功能啦,跟shiro没什么关系,因为这里是用token来作为凭证的,然后shiro中设置了开放用户登录的接口,重要的一点是,登录成功的话会根据用户身份生成一个token返回给前端。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 登录
* @param userQuery
* @param response
* @return
* @throws Exception
*/
@PostMapping("/login")
public Result login(@RequestBody UserQuery userQuery, ServletResponse response) throws Exception{
List<UserVo> userVo = userService.login(userQuery);
if(userVo.size() == 0){
//账号不存在
return ResultUtils.dataNotFoundError(AuthConstant.UNKNOWN_ACCOUNT);
}else if(!userVo.get(0).getLoginPWD().equals(userQuery.getLoginPWD())){
//密码错误
return ResultUtils.error(AuthConstant.WRONG_PASSWORD);
}else{
//通过认证, 生成签名
String token = userService.saveToken(userVo.get(0));
//token写入前端cookie
JwtAuthenticator.editCookieToken(response, token);
return ResultUtils.success(userVo);
}
}
/**
* 注销
* @param request
* @param response
* @return
*/
@PostMapping("/logout")
public Result logout(ServletRequest request, ServletResponse response){
String token = JwtAuthenticator.getRequestToken((HttpServletRequest)request);
String userName = JwtAuthenticator.getUsername(token);
List<UserVo> userVo = userService.qryUserByUserName(userName);
userService.deleteToken(userVo.get(0));
//前端token置空
JwtAuthenticator.editCookieToken(response, "");
return ResultUtils.success();
}
/**
* 查询当前用户,通过token解密查询
* @param request
* @return
*/
@PostMapping("/fetchCurrentUser")
public Result fetchCurrentUser(ServletRequest request){
String token = JwtAuthenticator.getRequestToken((HttpServletRequest)request);
String userName = JwtAuthenticator.getUsername(token);
List<UserVo> userVo = userService.qryUserByUserName(userName);
if(userVo.size() <= 0){
return ResultUtils.dataNotFoundError(AuthConstant.UNKNOWN_ACCOUNT);
}
return ResultUtils.success(userVo.get(0));
}
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
@Autowired
private PermissionDao permissionDao;
@Autowired
private JedisUtil jedisUtil;
@Override
//登录,根据用户信息查询用户并关联角色和权限
public List<UserVo> login(UserQuery userQuery){
Map<Long, UserVo> userVoMap = new HashMap<Long, UserVo>();
//根据查询条件查出用户
List<UserDto> userDtoList = new ArrayList<UserDto>();
if(userQuery.getUserName()!=null && !userQuery.getUserName().equals("")){
userDtoList = userDao.qryUserByUserName(userQuery.getUserName());
}
if(userDtoList.size() <= 0){
return new ArrayList<UserVo>(userVoMap.values());
}
List<Long> userIds = new ArrayList<Long>();
for(UserDto u : userDtoList){
userIds.add(u.getUserId());
userVoMap.put(u.getUserId(), new UserVo(u));
}
List<Long> roleIds = new ArrayList<Long>();
Map<Long, RoleVo> roleVoMap = new HashMap<Long, RoleVo>();
// 查出用户的全部角色,然后添加到用户的角色列表中
List<RoleDto> roleDtoList = roleDao.qryRoleByUserIds(userIds);
for(RoleDto role : roleDtoList) {
roleIds.add(role.getRoleId());
RoleVo roleVo = new RoleVo(role);
roleVoMap.put(role.getRoleId(), roleVo);
UserVo userVo = userVoMap.get(role.getUserId());
if (null == userVo) {
continue;
}
if (null == userVo.getRoleVoList()) {
userVo.setRoleVoList(new ArrayList<RoleVo>());
}
userVo.getRoleVoList().add(roleVo);
}
if(roleIds.size() <= 0){
return new ArrayList<UserVo>(userVoMap.values());
}
// 查出角色对应的资源权限,添加到对应的角色中
List<PermissionDto> permissionDtos = permissionDao.qryPermissionByRoleIds(roleIds);
for(PermissionDto permission : permissionDtos) {
RoleVo roleVo = roleVoMap.get(permission.getRoleId());
if (null == roleVo) {
continue;
}
List<PermissionVo> permissionVoList = roleVo.getPermissionVoList();
if (null == permissionVoList) {
permissionVoList = new ArrayList<PermissionVo>();
}
permissionVoList.add(DataTranslationUtils.trans(permission, PermissionVo.class));
roleVo.setPermissionVoList(permissionVoList);
}
return new ArrayList<UserVo>(userVoMap.values());
}
//根据用户名查询用户,不关联角色
@Override
public List<UserVo> qryUserByUserName(String userName) {
List<UserDto> userDtoList = userDao.qryUserByUserName(userName);
return DataTranslationUtils.trans(userDtoList, UserVo.class);
}
//处理token,调用生成token的方法,返回token,在这里引入了redis,如果有必要的话想将token保存到redis里面。
@Override
public String saveToken(UserVo userVo){
try{
Date setTime = new Date();
Date expireTime = new Date();
expireTime.setTime(setTime.getTime() + AuthConstant.EXPIRE_TIME);
String token = JwtAuthenticator.sign(userVo, expireTime);
//如果JedisPoll存在,将token存储起来。
if(jedisUtil.getJedisPool() != null){
TokenDto tokenDto = new TokenDto();
tokenDto.setUserId(userVo.getUserId());
tokenDto.setToken(token);
tokenDto.setUpdateTime(setTime);
tokenDto.setExpireTime(expireTime);
jedisUtil.set(
String.valueOf(userVo.getUserId()).getBytes(),
SerializeUtil.serialize(tokenDto),
JedisConfig.database);
}
return token;
}catch (Exception e){
}
return null;
}
//删除token,主要是用户注销的时候,设置token立马过期,并删除Jedis里面存储的token
@Override
public void deleteToken(UserVo userVo) {
Date currentTime = new Date();
JwtAuthenticator.sign(userVo, currentTime);
if(jedisUtil.getJedisPool() != null){
jedisUtil.del(JedisConfig.database,String.valueOf(userVo.getUserId()).getBytes());
}
}
}
/**
* String工具
*/
public class StringUtil {
/**
* 定义下划线
*/
private static final char UNDERLINE = '_';
/**
* String为空判断(不允许空格)
* @param str
* @return boolean
*/
public static boolean isBlank(String str) {
return str == null || "".equals(str.trim());
}
/**
* String不为空判断(不允许空格)
* @param str
* @return boolean
*/
public static boolean isNotBlank(String str) {
return !isBlank(str);
}
/**
* Byte数组为空判断
* @param bytes
* @return boolean
*/
public static boolean isNull(byte[] bytes) {
// 根据byte数组长度为0判断
return bytes == null || bytes.length == 0;
}
/**
* Byte数组不为空判断
* @param bytes
* @return boolean
*/
public static boolean isNotNull(byte[] bytes) {
return !isNull(bytes);
}
}
/**
* Json和Object的转换工具
*/
public class JsonConvertUtil {
/**
* JSON 转 Object
*/
public static <T> T jsonToObject(String pojo, Class<T> clazz) {
return JSONObject.parseObject(pojo, clazz);
}
/**
* Object 转 JSON
*/
public static <T> String objectToJson(T t){
return JSONObject.toJSONString(t);
}
}
/**
* 接口返回结果工具类
*/
public class ResultUtils {
public static Result success(Object object) {
return new Result()
.setCode(ResultEnum.SUCCESS.getCode())
.setMessage(ResultEnum.SUCCESS.getMessage())
.setData(object);
}
public static Result success() {
return success(null);
}
public static Result error(String errMsg) {
return error(ResultEnum.FAIL.getCode(), ResultEnum.FAIL.getMessage(), errMsg);
}
public static Result dataNotFoundError(String errMsg) {
return error(ResultEnum.DATA_NOT_FOUND.getCode(), ResultEnum.DATA_NOT_FOUND.getMessage(), errMsg);
}
public static Result unauthorizedError(String errMsg) {
return error(ResultEnum.UNAUTHORIZED.getCode(), ResultEnum.UNAUTHORIZED.getMessage(), errMsg);
}
public static Result forbiddenError(String errMsg) {
return error(ResultEnum.FORBIDDEN.getCode(), ResultEnum.FORBIDDEN.getMessage(), errMsg);
}
public static Result paramNotVaildError(String errMsg) {
return error(ResultEnum.PARAM_NOT_VALID.getCode(), ResultEnum.PARAM_NOT_VALID.getMessage(), errMsg);
}
public static Result paramTypeConversionError(String errorMsg) {
return error(ResultEnum.TYPE_CONVERTION_ERROR.getCode(), ResultEnum.TYPE_CONVERTION_ERROR.getMessage(), errorMsg);
}
public static Result numberFormatError(String errorMsg) {
return error(ResultEnum.NUMBER_FORMAT_ERROR.getCode(), ResultEnum.NUMBER_FORMAT_ERROR.getMessage(), errorMsg);
}
public static Result internalServerError(String errorMsg) {
return error(ResultEnum.INTERNAL_SERVER_ERROR.getCode(), ResultEnum.INTERNAL_SERVER_ERROR.getMessage(), errorMsg);
}
public static Result interfaceNotFoundError(String errorMsg) {
return error(ResultEnum.NOT_FOUND.getCode(), ResultEnum.NOT_FOUND.getMessage(), errorMsg);
}
private static Result error(Integer code, String message, String errMsg) {
return new Result()
.setCode(code)
.setMessage(message)
.setData(errMsg);
}
}
做这个功能的过程中参考了好多大神的项目代码,在这里表示感谢(链接找不到了)。给自己写了一周的成果做一点记录,不然过段时间又忘了,还是自己太菜了,以后还是要多学习,每天都要进步一点点!