项目集成SpringSecurity
在第一章我们是基于SpringSecurity、JWT技术实现前后端无状态化认证授权,而我们当前的项目是前后端分离的架构,同样也可借助Security框架和Jwt实现前后端的无状态认证授权操作;
1、项目自定义认证过滤器
1.1 依赖导入
在stock_backend工程导入SpringSecurity启动依赖:
<!--引入security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.2 自定义认证过虑器
当前项目中认证登录信息的合法性,除了用户名、密码外,还需要校验验证码,所以认证过滤器需要注入redis模板对象:
package com.itheima.stock.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.itheima.stock.constant.StockConstant;
import com.itheima.stock.security.detail.LoginUserDetail;
import com.itheima.stock.utils.JwtTokenUtil;
import com.itheima.stock.vo.req.LoginReqVo;
import com.itheima.stock.vo.resp.LoginRespVoExt;
import com.itheima.stock.vo.resp.R;
import com.itheima.stock.vo.resp.ResponseCode;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* @author by itheima
* @Date 2022/7/14
* @Description
*/
public class JwtLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private RedisTemplate redisTemplate;
/**
* 通过setter方法注解redis模板对象
* @param redisTemplate
*/
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 通过构造器传入自定义的登录地址
* @param loginUrl
*/
public JwtLoginAuthenticationFilter(String loginUrl) {
super(loginUrl);
}
/**
* 用户认证处理的方法
* @param request
* @param response
* @return
* @throws AuthenticationException
* @throws IOException
* @throws ServletException
* 我们约定请求方式必须是post方式,且请求的数据时json格式
* 约定请求是账户key:username 密码:password
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//判断请求方法必须是post提交,且提交的数据的内容必须是application/json格式的数据
if (!request.getMethod().equals("POST") ||
! (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE))) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//获取请求参数
//获取reqeust请求对象的发送过来的数据流
ServletInputStream in = request.getInputStream();
//将数据流中的数据反序列化成Map
LoginReqVo vo = new ObjectMapper().readValue(in, LoginReqVo.class);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("utf-8");
//1.判断参数是否合法
if (vo==null || StringUtils.isBlank(vo.getUsername())
|| StringUtils.isBlank(vo.getPassword())
|| StringUtils.isBlank(vo.getSessionId()) || StringUtils.isBlank(vo.getCode())) {
R<Object> resp = R.error(ResponseCode.USERNAME_OR_PASSWORD_ERROR.getMessage());
response.getWriter().write(new ObjectMapper().writeValueAsString(resp));
return null;
}
//从程序执行的效率看,先进行校验码的校验,成本较低
String rCheckCode =(String) redisTemplate.opsForValue().get(StockConstant.CHECK_PREFIX + vo.getSessionId());
if (rCheckCode==null || ! rCheckCode.equalsIgnoreCase(vo.getCode())) {
//响应验证码输入错误
R<Object> resp = R.error(ResponseCode.CHECK_CODE_ERROR.getMessage());
response.getWriter().write(new ObjectMapper().writeValueAsString(resp));
return null;
}
String username = vo.getUsername();
//username = (username != null) ? username : "";
username = username.trim();
String password = vo.getPassword();
//password = (password != null) ? password : "";
//将用户名和密码信息封装到认证票据对象下
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
//setDetails(request, authRequest);
//调用认证管理器认证指定的票据对象
return this.getAuthenticationManager().authenticate(authRequest);
}
}
2、自定义用户详情服务
上一小结,我们完成了认证过滤器的开发,认证过程中认证管理器AutenticationManager底层会调用用户详情服务对象获取用户详情信息,所以接下来我们需要实现用户详情服务;
权限表注意事项:
2.1 自定义UserDetail认证详情信息类
package com.itheima.stock.security.detail;
import com.itheima.stock.vo.resp.PermissionRespNodeVo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* @author by itheima
* @Date 2022/7/14
* @Description 自定义用户认证详情类
*/
@Data//setter getter toString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LoginUserDetail implements UserDetails {
/**
* 用户名称
*/
private String username;
// @Override
// public String getUsername() {
// return null;
// }
/**
* 密码
*/
private String password;
// @Override
// public String getPassword() {
// return null;
// }
/**
* 权限信息
*/
private List<GrantedAuthority> authorities;
// @Override
// public Collection<? extends GrantedAuthority> getAuthorities() {
// return null;
// }
/**
* 账户是否过期
*/
private boolean isAccountNonExpired=true;
// @Override
// public boolean isAccountNonExpired() {
// return false;
// }
/**
* 账户是否被锁定
* true:没有被锁定
*/
private boolean isAccountNonLocked=true;
// @Override
// public boolean isAccountNonLocked() {
// return false;
// }
/**
* 密码是否过期
* true:没有过期
*/
private boolean isCredentialsNonExpired=true;
// @Override
// public boolean isCredentialsNonExpired() {
// return false;
// }
/**
* 账户是否禁用
* true:没有禁用
*/
private boolean isEnabled=true;
// @Override
// public boolean isEnabled() {
// return false;
// }
/**
* 用户ID
*/
private String id;
/**
* 电话
*/
private String phone;
/**
* 昵称
*/
private String nickName;
/**
* 真实姓名
*/
private String realName;
/**
* 性别(1.男 2.女)
*/
private Integer sex;
/**
* 账户状态(1.正常 2.锁定 )
*/
private Integer status;
/**
* 邮箱
*/
private String email;
/**
* 权限树,不包含按钮相关权限信息
*/
private List<PermissionRespNodeVo> menus;
/**
* 按钮权限树
*/
private List<String> permissions;
}
2.2 自定义UserDetailsService实现
package com.itheima.stock.security.service;
import com.google.common.base.Strings;
import com.itheima.stock.mapper.SysPermissionMapper;
import com.itheima.stock.mapper.SysRoleMapper;
import com.itheima.stock.mapper.SysUserMapperExt;
import com.itheima.stock.pojo.entity.SysPermission;
import com.itheima.stock.pojo.entity.SysRole;
import com.itheima.stock.pojo.entity.SysUser;
import com.itheima.stock.security.detail.LoginUserDetail;
import com.itheima.stock.service.PermissionService;
import com.itheima.stock.vo.resp.PermissionRespNodeVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author by itheima
* @Date 2022/7/13
* @Description 定义获取用户合法详情信息的服务
*/
@Component
public class LoginUserDetailService implements UserDetailsService {
@Autowired
private SysUserMapperExt sysUserMapperExt;
@Autowired
private SysPermissionMapper sysPermissionMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
@Autowired
private PermissionService permissionService;
/**
* 当用户登录认证是,底层会自动调用MyUserDetailService#loadUserByUsername()把登录的账户名称传入
* 根据用户名称获取用户的详情信息:用户名 加密密码 权限集合,还包含前端需要的侧边栏树 、前端需要的按钮权限标识的集合等
* @param loginName
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
//2.根据用户名查询用户信息
SysUser dbUser= sysUserMapperExt.findUserByUserName(username);
//3.判断查询的用户信息
if (dbUser==null) {
throw new UsernameNotFoundException("用户不存在");
}
//4.2 成功则返回用户的正常信息
//获取指定用户的权限集合 添加获取侧边栏数据和按钮权限的结合信息
List<SysPermission> permissions = permissionService.getPermissionByUserId(dbUser.getId());
//前端需要的获取树状权限菜单数据
List<PermissionRespNodeVo> tree = permissionService.getTree(permissions, 0l, true);
//前端需要的获取菜单按钮集合
List<String> authBtnPerms = permissions.stream()
.filter(per -> !Strings.isNullOrEmpty(per.getCode()) && per.getType() == 3)
.map(per -> per.getCode()).collect(Collectors.toList());
//5.组装后端需要的权限标识
//5.1 获取用户拥有的角色
List<SysRole> roles = sysRoleMapper.getRoleByUserId(dbUser.getId());
//5.2 将用户的权限标识和角色标识维护到权限集合中
List<String> perms=new ArrayList<>();
permissions.stream().forEach(per->{
if (StringUtils.isNotBlank(per.getPerms())) {
perms.add(per.getPerms());
}
});
roles.stream().forEach(role->{
perms.add("ROLE_"+role.getName());
});
String[] permStr=perms.toArray(new String[perms.size()]);
//5.3 将用户权限标识转化成权限对象集合
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(permStr);
//6.封装用户详情信息实体对象
LoginUserDetail loginUserDetail = new LoginUserDetail();
//将用户的id nickname等相同属性信息复制到详情对象中
BeanUtils.copyProperties(dbUser,loginUserDetail);
loginUserDetail.setMenus(tree);
loginUserDetail.setAuthorities(authorityList);
loginUserDetail.setPermissions(authBtnPerms);
return loginUserDetail;
}
}
2.3 完善相关mapper
A.定义根据用户名查询用户信息的接口方法
在SysUserMapper定义方法:
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
SysUser findUserByUserName(@Param("username") String username);
绑定xml:
<select id="findUserByUserName" resultMap="BaseResultMap">
select <include refid="Base_Column_List"/> from sys_user where username=#{username}
</select>
B.定义根据用户id查询角色信息的接口方法
在SysRoleMapper定义方法:
/**
* 根据用户id查询角色信息
* @param userId
* @return
*/
List<SysRole> getRoleByUserId(@Param("userId") Long userId);
绑定xml:
<select id="getRoleByUserId" resultMap="BaseResultMap">
SELECT
r.*
FROM
sys_user_role AS ur,
sys_role AS r
WHERE
ur.role_id = r.id
AND ur.user_id = #{userId}
</select>
C.定义根据用户id查询权限信息的接口方法
在SysPermissionMapper定义方法: