目录
项目没集成SpringSecurity框架的实现
项目之前的登录接口
LoginReqVo 接收前端的数据类型
LoginRespVo返回给前端的数据
项目集成SpringSecurity
第一步:导入依赖
第二步:创建security包结构
第三步:实现认证过滤器
第一步:自定义认证过滤器继承AbstractAuthenticationProcessingFilter
第二步:自定义类实现UserDetailsService接口
第三步:自定义类实现UserDetail接口,自定义设置一个用户详情对象
第四步:编写认证成功后回调的方法和认证失败后回调的方法
第五步:在配置类定义这个自定义过滤器,并定义登录接口(/api/login)
第六步:测试
第四步:实现授权过滤器
第一步:自定义类继承OncePerRequestFilter
第二步:在配置类中定义这个自定义授权过滤器
第三步:测试
第五步:实现拒绝处理器
第一步:自定义类实现AccessDeniedHandler接口
第二步:自定义类实现 AuthenticationEntryPoint接口
第三步:在配置类中定义这两个处理器
第四步:测试
第六步:给接口配置权限信息标识
我们需要在项目中集成SpringSecurity框架,主要的步骤就是在登录路径(/api/login)设置认证过滤器
项目没集成SpringSecurity框架的实现
项目之前的登录接口
@ApiImplicitParams({
@ApiImplicitParam(paramType = "body", dataType = "LoginReqVo", name = "loginReqVo", value = "用户的用户名,密码,验证码", required = true)
})
@ApiOperation(value = "登录功能", notes = "用户登录功能", httpMethod = "POST")
@PostMapping("/login")
public R<LoginRespVo> login(@RequestBody LoginReqVo loginReqVo){//接收前端发送的json数据并封装到LoginReqVo类对象
return userService.login(loginReqVo);
}
LoginReqVo 接收前端的数据类型
/**
* 接收登录功能发过来的json数据,变量名字必须与json数据的key值一样
*/
@Data
@ApiModel(description = "用户登录要的信息")
public class LoginReqVo {
@ApiModelProperty("用户名字")
private String username;
@ApiModelProperty("用户明文密码")
private String password;
//验证码
@ApiModelProperty("验证码")
private String code;
//sessionId
@ApiModelProperty("sessionId")
private String sessionId;
}
LoginRespVo返回给前端的数据
/**
* 通过登录功能返回给前端的数据类型
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ApiModel(description = "用户的基本信息")
public class LoginRespVo {
/**
* 用户id
* TODO:由于Long类型的数字长度过长时,发送给前端的时候会数据丢失,所以需要把Long类型的id变成String类型
* TODO:再发送给前端
*/
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty(value = "用户id")
private Long id;
//用户名字
@ApiModelProperty(value = "用户名字")
private String username;
//昵称
@ApiModelProperty(value = "用户昵称")
private String nickName;
//电话
@ApiModelProperty(value = "用户电话")
private String phone;
@ApiModelProperty("真实名称")
private String realName;
@ApiModelProperty("性别")
private Integer sex;
@ApiModelProperty("状态")
private Integer status;
@ApiModelProperty("邮件")
private String email;
@ApiModelProperty("侧边栏权限树(不包含按钮权限)")
private List<MenuDomain>menus;
@ApiModelProperty("按钮权限标识")
private List<String>permissions;
/**
* 用户登录成功后,将用户的id和name信息经过base64编码,作为票据token响应给前端
* accessToken会保存在浏览器前端,同时前端访问后端接口时,会在请求头中携带票据信息,其中请求头中的key为Authorization,value为accessToken对应的值;
* 后端只需从请求头中获取Authorization对应的值,即可知道当前来自哪个用户的访问了
*/
@ApiModelProperty("票据token")
private String accessToken;
}
项目集成SpringSecurity
第一步:导入依赖
这里顺便把jjwt依赖也导入,方便以后生成jwt票据
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!-- 导入springSecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
第二步:创建security包结构
第三步:实现认证过滤器
第一步:自定义认证过滤器继承AbstractAuthenticationProcessingFilter
在attemptAuthentication尝试认证方法中
1.先判断请求方法是不是post方式,和请求的数据是不是json数据,如果不是就抛出异常
2.解析前端传入的json数据成 LoginReqVo接收前端数据的数据类型
//获取reqeust请求对象的发送过来的数据流
ServletInputStream in = request.getInputStream();
//将数据流中的数据反序列化成获取前端数据的请求封装类
LoginReqVo loginReqVo = new ObjectMapper().readValue(in, LoginReqVo.class);
3.然后根据sessionId去Redis查看验证码是否正确,如果正确就把用户的用户名和用户的明文密码封装到用户票据类型对象( UsernamePasswordAuthenticationToken)中,然后调用认证管理器进行认证
/**
* 自定义过滤器,继承AbstractAuthenticationProcessingFilter类
* 核心作用是 认证用户信息,如果认证成功就颁发票据
*/
public class JwtLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private RedisTemplate redisTemplate;
private static final String USER_NAME="username";
private static final String PASSWORD="password";
/**
* 自定义构造器,传入认证登录的url地址
* @param loginUrl 登录的url地址
*/
public JwtLoginAuthenticationFilter(String loginUrl, RedisTemplate redisTemplate) {
super(loginUrl);
this.redisTemplate=redisTemplate;
}
/**
* 尝试去认证的方法,把前端返回的用户名和密码封装到UsernamePasswordAuthenticationToken认证凭对象;
* data:{"username":"hhh","password":"123456"}
* @param request
* @param response
* @return
* @throws AuthenticationException
* @throws IOException
* @throws ServletException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//判断请求方法必须是post提交,且提交的数据的内容必须是application/json格式的数据
if (!request.getMethod().equalsIgnoreCase("POST") || !MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(request.getContentType()))
{
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//获取post请求ajax前端传入的json数据,并解析成map集合
//获取请求参数
//获取reqeust请求对象的发送过来的数据流
ServletInputStream in = request.getInputStream();
//将数据流中的数据反序列化成获取前端数据的请求封装类
LoginReqVo loginReqVo = new ObjectMapper().readValue(in, LoginReqVo.class);
//设置返回的格式
response.setCharacterEncoding("UTF-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
//判断参数是否合法
if(loginReqVo==null|| StringUtils.isBlank(loginReqVo.getUsername())||StringUtils.isBlank(loginReqVo.getPassword())){
R<Object> error = R.error(ResponseCode.DATA_ERROR);
//将对象序列化成json数据
String jsonData = new ObjectMapper().writeValueAsString(error);
response.getWriter().write(jsonData);
//直接结束此方法,不执行下面的步骤
return null;
}
//判断验证码是否存在
if(StringUtils.isBlank(loginReqVo.getCode())){
R<Object> error = R.error(ResponseCode.CHECK_CODE_NOT_EMPTY);
//将对象序列化成json数据
String jsonData = new ObjectMapper().writeValueAsString(error);
response.getWriter().write(jsonData);
//直接结束此方法,不执行下面的步骤
return null;
}
//判断验证码是否正确,根据sessionId(key)去redis查找验证码(value),TODO:别忘了验证码前缀
String code = (String) redisTemplate.opsForValue().get(StockConstant.CHECK_PREFIX+loginReqVo.getSessionId());
if(StringUtils.isBlank(code)){//取出来的验证码为空
R<Object> error = R.error(ResponseCode.CHECK_CODE_TIMEOUT);
//将对象序列化成json数据
String jsonData = new ObjectMapper().writeValueAsString(error);
response.getWriter().write(jsonData);
//直接结束此方法,不执行下面的步骤
return null;
}
if(!code.equalsIgnoreCase(loginReqVo.getCode())){//忽略大小写比较
R<Object> error = R.error(ResponseCode.CHECK_CODE_ERROR);
//将对象序列化成json数据
String jsonData = new ObjectMapper().writeValueAsString(error);
response.getWriter().write(jsonData);
//直接结束此方法,不执行下面的步骤
return null;
}
String username = loginReqVo.getUsername();
username = (username != null) ? username : "";
username = username.trim();
String password = loginReqVo.getPassword();
password = (password != null) ? password : "";
//将用户名和密码信息封装到认证票据对象下
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//调用认证管理器认证指定的票据对象
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 认证成功后执行的方法
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
}
/**
* 认证失败执行的方法
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
}
}
第二步:自定义类实现UserDetailsService接口
认证管理器会调用UserDetailsService的loadUserByUsername的方法
然后在这个方法的方法参数中,会从用户票据对象的username,获取数据值
1.然后通过这个用户名去数据查找这个用户的所有详细信息
2.封装要给前端返回的权限菜单,和权限按钮
//获取用户的所有权限信息
List<SysPermission> perms = sysPermissionMapper.getAllPermission(dbUser.getUsername());//通过工具类获取权限实体类集合的权限菜单,不包括权限按钮,0作为pid(父亲id)
List<MenuDomain>menus = ParsePermission.recursionGetMenus(perms, 0L);
//通过工具类获取权限实体类集合的权限按钮(type==3)
List<String> permissions = ParsePermission.getButtonCode(perms);
工具类
public class ParsePermission {
/**
* 获取权限实体类集合中的所有权限按钮
* @param permissions 权限实体类集合
*/
public static List<String>getButtonCode(List<SysPermission>permissions){
return permissions.stream().filter(permission->
!Strings.isNullOrEmpty(permission.getCode())&&permission.getType()==3//只有type==3才是button权限按钮
).map(SysPermission::getCode)//获取SysPermission类中的code属性,并收集起来
.collect(Collectors.toList());
}
/**
* 获取侧边栏权限树(不包含按钮权限)
* @param permissions 权限实体对象集合
* @param pid 父亲id
*/
public static List<MenuDomain> recursionGetMenus(List<SysPermission>permissions,Long pid){
return permissions.stream()
.filter(permission -> permission.getPid().equals(pid))//父id相同
.filter(permission->permission.getType()!=3)//类型不是3,即不是按按钮
.map(permission -> {//组装MenuDomain菜单类型
return MenuDomain.builder()
.id(permission.getId())
.title(permission.getTitle())
.icon(permission.getIcon())
.path(permission.getUrl())
.name(permission.getName())
//递归调用此方法,获取以这个权限id作为父亲id的所有子权限
.children(recursionGetMenus(permissions, permission.getId()))
.build();
}).collect(Collectors.toList());
}
}
3.封装springSecurity需要的权限表示,包括ROLE_角色名称 和 权限自身
//获取SpringSecurity的权限表示:ROLE_角色名称 和 权限自身
ArrayList<String>authorities=new ArrayList<>();
List<String> ps = perms.stream()
.filter(p -> StringUtils.isNotBlank(p.getPerms()))
.map(SysPermission::getPerms).collect(Collectors.toList());List<SysRole>roles=sysRoleMapper.findRoleByUserId(dbUser.getId());
List<String> rs = roles.stream().map(r -> "ROLE_" + r.getName()).collect(Collectors.toList());authorities.addAll(ps);
authorities.addAll(rs);
4. 将该用户拥有的权限集合转换成权限对象
//将该用户拥有的权限集合转换成权限对象
//先把集合变成String数组
String[] psArray = authorities.toArray(new String[authorities.size()]);
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(psArray);
5. 构建用户详情对象,之后认证管理器会使用这个详情对象的密文密码与用户票据对象的明文密码进行比对
LoginUserDetail loginUserDetail = new LoginUserDetail();
BeanUtils.copyProperties(dbUser,loginUserDetail);
loginUserDetail.setAuthorities(authorityList);
loginUserDetail.setMenus(menus);
loginUserDetail.setPermissions(permissions);
/**
* 定义从数据库获取用户详情服务bean
*/
@Service("userDetailsService")
public class LoginUserDetailService implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysPermissionMapper sysPermissionMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
/**
* 根据用户票据类对象提供的用户名去数据库查找用户的详细信息(用户名,用户密文密码,用户的权限集合)
* @param username 用户票据类对象提供的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.根据用户名去数据库查找用户的详细信息
SysUser dbUser = sysUserMapper.findUserInfoByUserName(username);
if(dbUser==null){
throw new UsernameNotFoundException("该用户不存在");
}
//2.组装UserDetails对象
//springSecurity默认的UserDetails实现类User只有用户名,用户密码密码,用户的权限集合
//但是要返回给前端的信息还包括id email,menus等,所以我们要自定义实现类
//获取用户的所有权限信息
List<SysPermission> perms = sysPermissionMapper.getAllPermission(dbUser.getUsername());
//通过工具类获取权限实体类集合的权限菜单,不包括权限按钮,0作为pid(父亲id)
List<MenuDomain>menus = ParsePermission.recursionGetMenus(perms, 0L);
//通过工具类获取权限实体类集合的权限按钮(type==3)
List<String> permissions = ParsePermission.getButtonCode(perms);
//获取SpringSecurity的权限表示:ROLE_角色名称 和 权限自身
ArrayList<String>authorities=new ArrayList<>();
List<String> ps = perms.stream()
.filter(p -> StringUtils.isNotBlank(p.getPerms()))
.map(SysPermission::getPerms).collect(Collectors.toList());
List<SysRole>roles=sysRoleMapper.findRoleByUserId(dbUser.getId());
List<String> rs = roles.stream().map(r -> "ROLE_" + r.getName()).collect(Collectors.toList());
authorities.addAll(ps);
authorities.addAll(rs);
//将该用户拥有的权限集合转换成权限对象
//先把集合变成String数组
String[] psArray = authorities.toArray(new String[authorities.size()]);
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(psArray);
//构建用户详情对象,之后会使用这个详情对象的密文密码与用户票据对象的明文密码进行比对
LoginUserDetail loginUserDetail = new LoginUserDetail();
BeanUtils.copyProperties(dbUser,loginUserDetail);
loginUserDetail.setAuthorities(authorityList);
loginUserDetail.setMenus(menus);
loginUserDetail.setPermissions(permissions);
return loginUserDetail;
}
}
第三步:自定义类实现UserDetail接口,自定义设置一个用户详情对象
认证管理器在调用完 loadUserByUsername方法获取用户详细信息并封装到用户详情对象后,认证管理器会自动使用BrcryptPassword(需要我们定义这个bean)对用户票据对象的明文密码和用户详情对象的密文密码进行比对
比对成功后会回调认证过滤器的成功认证的方法,我们只需要在这个方法中返回前端所需要的json数据即可,但是默认的User(默认实现UserDetail接口的用户详情对象)只有用户名,用户密文密码,用户权限集合三个成员变量,无法满足给前端返回的数据,所以我们需要自定义一个类实现UserDetail接口
/**
* 自定义用户详情对象类
*/
@Data
public class LoginUserDetail implements UserDetails {
/**
* 用户名
*/
private String username;
/**
* 用户密文密码
*/
private String password;
/**
* 用户的权限集合
*/
private List<GrantedAuthority>authorities;
//@JsonSerialize(using = ToStringSerializer.class)
/**
* 用户id
*/
private Long id;
/**
* 用户昵称
*/
private String nickName;
/**
* 用户电话
*/
private String phone;
/**
* 真实名称
*/
private String realName;
/**
* 性别
*/
private Integer sex;
/**
* 状态
*/
private Integer status;
/**
* 邮件
*/
private String email;
/**
* 侧边栏权限树(不包含按钮权限)
*/
private List<MenuDomain>menus;
/**
* 按钮权限标识
*/
private List<String>permissions;
/* @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}*/
/* @Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return null;
}*/
/**
* true:账号没有过期
*/
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;
}*/
}
第四步:编写认证成功后回调的方法和认证失败后回调的方法
在successfulAuthentication()成功认证的回调方法中
获取用户详情对象的权限集合对象,使用工具类把用户名和权限封装成token票据
工具类
public class JwtTokenUtil { // Token请求头 public static final String TOKEN_HEADER = "authorization"; // Token前缀 public static final String TOKEN_PREFIX = "Bearer "; // 签名主题 public static final String SUBJECT = "JRZS"; // 过期时间,单位毫秒 public static final long EXPIRITION = 1000 * 60 * 60* 24 * 7; // 应用密钥 public static final String APPSECRET_KEY = "hhha"; // 角色权限声明 private static final String ROLE_CLAIMS = "role"; /** * 生成Token */ public static String createToken(String username,String role) { Map<String,Object> map = new HashMap<>(); map.put(ROLE_CLAIMS, role); String token = Jwts .builder() .setSubject(username) .setClaims(map) .claim("username",username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)) .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact(); return token; } /** * 校验Token */ public static Claims checkJWT(String token) { try { final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 从Token中获取用户名 */ public static String getUsername(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get("username").toString(); } /** * 从Token中获取用户角色 */ public static String getUserRole(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get("role").toString(); } /** * 校验Token是否过期 */ public static boolean isExpiration(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.getExpiration().before(new Date()); } }
/**
* 认证成功后执行的方法
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//获取封装后的用户对象
//这一个principal对象里的密码为null
LoginUserDetail principal=(LoginUserDetail)authResult.getPrincipal();
String username = principal.getUsername();
Collection<GrantedAuthority> authorities = principal.getAuthorities();
//使用工具类生成jwt令牌,authorities.toString()把权限集合变成["P1","ROLE_ADMIN]类型
//构建JwtToken 加入权限信息是为了将来访问时,jwt解析获取当前用户对应的权限,做授权的过滤
String token = JwtTokenUtil.createToken(username, authorities.toString());
//设置返回数据格式
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
//设置返回数据编码格式
response.setCharacterEncoding("UTF-8");
//构建响应实体对象
LoginRespVo loginRespVo = new LoginRespVo();
BeanUtils.copyProperties(principal,loginRespVo);
loginRespVo.setAccessToken(token);//设置票据
R<LoginRespVo> info = R.ok(loginRespVo);
//把类对象变成json数据
String jsonData = new ObjectMapper().writeValueAsString(info);
//将json数据返回
response.getWriter().write(jsonData);
}
/**
* 认证失败执行的方法
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//设置响应数据为json
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
//设置响应数据格式
response.setCharacterEncoding("UTF-8");
//设置响应的数据
R<Object> info = R.error(ResponseCode.ERROR);
//将数据封装成json数据
String jsonData = new ObjectMapper().writeValueAsString(info);
response.getWriter().write(jsonData);
}
第五步:在配置类定义这个自定义过滤器,并定义登录接口(/api/login)
@Configuration
@EnableWebSecurity//开启web安全设置生效
//开启SpringSecurity相关注解支持
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RedisTemplate redisTemplate;
/**
* 定义公共的无需被拦截的资源
*/
private String[] getPubPath(){
//公共访问资源
String[] urls = {
"/**/*.css","/**/*.js","/favicon.ico","/doc.html",
"/druid/**","/webjars/**","/v2/api-docs","/api/captcha",
"/swagger/**","/swagger-resources/**","/swagger-ui.html"
};
return urls;
}
//TODO:返回一个BCryptPasswordEncoder密码加密类类,让SpringSecurity自动调用把密码密文和明文进行匹配
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 自定义认证过滤器
* 隐含:如果认证成功,则在安全上下文中维护认证相关信息
* 如果安全上下文中存在认证相关信息,则默认的UserNamePasswordAuthenticationFilter认证过滤器就不会执行
* 所以执行顺序,自定义的过滤器在前,默认的过滤器在后
* @return
* @throws Exception
*/
@Bean
public JwtLoginAuthenticationFilter jwtLoginAuthenticationFilter() throws Exception {
JwtLoginAuthenticationFilter jwtLoginAuthenticationFilter = new JwtLoginAuthenticationFilter("/api/login", redisTemplate);
//设置认证管理器
jwtLoginAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
return jwtLoginAuthenticationFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//登出功能,指定登录的url地址
http.logout().logoutUrl("/api/logout").invalidateHttpSession(true);
//开启允许iframe 嵌套。security默认禁用ifram跨域与缓存
http.headers().frameOptions().disable().cacheControl().disable();
//session禁用
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();//禁用跨站请求伪造
http.authorizeRequests()//对资源进行认证处理
.antMatchers(getPubPath()).permitAll()//公共资源都允许访问
.anyRequest().authenticated(); //除了上述资源外,其它资源,只有 认证通过后,才能有权访问
//对于自定义过滤器,需要创建一个实例,所以用方法返回一个自定义认证过滤器
http.addFilterBefore(jwtLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
第六步:测试
访问/api/login接口,获取票据
第四步:实现授权过滤器
第一步:自定义类继承OncePerRequestFilter
如果token不为null且token解析正确
就会解析出用户名和用户权限集合,封装到用户票据对象( UsernamePasswordAuthenticationToken)中,然后把这个对象放到安全上下文中,后续的过滤器,比如:认证过滤器如果发现安全上下文中存在token票据对象,就不会进行重新认证
/**
* 定义授权过滤器,本质就是获取一切请求头的token信息,进行校验
*/
public class JwtAuthorizationFilter extends OncePerRequestFilter {
/**
* 过滤中执行的方法
* @param request
* @param response
* @param filterChain 过滤器链
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1.从请求头中获取token字符串
String tokenStr = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
//2.合法性判断
//2.1 判断票据信息是否为空
if(StringUtils.isBlank(tokenStr)){
//如果票据为空,则放行,但是此时安全上下文中没有认证成功的票据,后续的过滤器如果得不到票据,后面的认证过滤器会进行拦截
filterChain.doFilter(request,response);
return;
}
//2.2检查票据是否合法,解析失败
Claims claims = JwtTokenUtil.checkJWT(tokenStr);
if(claims==null){
//票据不合法,直接让过滤器终止,不放行即可
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
R<Object> error = R.error(ResponseCode.INVALID_TOKEN);
//把数据序列化成json格式
String jsonData = new ObjectMapper().writeValueAsString(error);
response.getWriter().write(jsonData);
return;
}
//2.3
//获取用户名
String username = JwtTokenUtil.getUsername(tokenStr);
//获取用户权限集合 "["P1","ROLE_ADMIN"]"
String roles = JwtTokenUtil.getUserRole(tokenStr);
//解析用户权限字符串
String stripStr = StringUtils.strip(roles, "[]");
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(stripStr);
//3.组装数据到UsernamePasswordAuthenticationToken票据对象中
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,null,authorities);
//4.将封装的认证票据存入security安全上下文中,这样后续的过滤器就可以直接从安全上下文中获取用户的相关权限信息
//TODO:以线程为维度:当前访问结束,那么线程回收,安全上下文中的票据也会回收,下次访问时需要重新解析
SecurityContextHolder.getContext().setAuthentication(token);
//5.放行请求,后续的过滤器,比如:认证过滤器如果发现安全上下文中存在token票据对象,就不会进行重新认证
filterChain.doFilter(request,response);
}
}
第二步:在配置类中定义这个自定义授权过滤器
/**
* 维护一个授权过滤器bean,检查jwt票据是否有效,并做相关处理
*/
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter(){
return new JwtAuthorizationFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//登出功能,指定登录的url地址
http.logout().logoutUrl("/api/logout").invalidateHttpSession(true);
//开启允许iframe 嵌套。security默认禁用ifram跨域与缓存
http.headers().frameOptions().disable().cacheControl().disable();
//session禁用
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();//禁用跨站请求伪造
http.authorizeRequests()//对资源进行认证处理
.antMatchers(getPubPath()).permitAll()//公共资源都允许访问
.anyRequest().authenticated(); //除了上述资源外,其它资源,只有 认证通过后,才能有权访问
//对于自定义过滤器,需要创建一个实例,所以用方法返回一个自定义认证过滤器
http.addFilterBefore(jwtLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//配置授权过滤器,它是资源安全的屏障,优先级最高,所以需要在自定义的认证过滤器之前
http.addFilterBefore(jwtAuthorizationFilter(), JwtLoginAuthenticationFilter.class);
}
第三步:测试
在请求头中加入 key为:authorization value为:刚才访问/api/login登录接口返回的票据信息
第五步:实现拒绝处理器
第一步:自定义类实现AccessDeniedHandler接口
这个类就是用户没有权限访问接口时执行处理器
/**
* 自定义用户没有权限访问一个资源接口时要执行的处理器
*/
@Slf4j
public class StockAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
log.warn("当前用户无法访问资源,原因:{}",e.getMessage());
//向前端返回用户没有权限的信息
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
R<Object> error = R.error(ResponseCode.NOT_PERMISSION);
//将数据序列化成json数据
String jsonData = new ObjectMapper().writeValueAsString(error);
//向前端响应此json数据
response.getWriter().write(jsonData);
}
}
第二步:自定义类实现 AuthenticationEntryPoint接口
这个类就是(没有进行认证登录)匿名用户访问资源接口时执行的处理器
/**
* 自定义一个匿名用户(此用户没有进行认证)拒绝的处理器
*/
@Slf4j
public class StockAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.warn("匿名用户无法访问该资源,原因:{}",e.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
R<Object> error = R.error(ResponseCode.ANONMOUSE_NOT_PERMISSION);
String jsonData = new ObjectMapper().writeValueAsString(error);
response.getWriter().write(jsonData);
}
}
第三步:在配置类中定义这两个处理器
@Override
protected void configure(HttpSecurity http) throws Exception {
//登出功能,指定登录的url地址
http.logout().logoutUrl("/api/logout").invalidateHttpSession(true);
//开启允许iframe 嵌套。security默认禁用ifram跨域与缓存
http.headers().frameOptions().disable().cacheControl().disable();
//session禁用
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();//禁用跨站请求伪造
http.authorizeRequests()//对资源进行认证处理
.antMatchers(getPubPath()).permitAll()//公共资源都允许访问
.anyRequest().authenticated(); //除了上述资源外,其它资源,只有 认证通过后,才能有权访问
//对于自定义过滤器,需要创建一个实例,所以用方法返回一个自定义认证过滤器
http.addFilterBefore(jwtLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//配置授权过滤器,它是资源安全的屏障,优先级最高,所以需要在自定义的认证过滤器之前
http.addFilterBefore(jwtAuthorizationFilter(), JwtLoginAuthenticationFilter.class);
//配置用户没有权限时的处理器
http.exceptionHandling().accessDeniedHandler(new StockAccessDeniedHandler())
.authenticationEntryPoint(new StockAuthenticationEntryPoint());
}
第四步:测试
把请求头去掉就是匿名用户
第六步:给接口配置权限信息标识
使用@PreAuthorize注解
注意需要和数据库中的权限一样,即与用户票据对象中的权限集合的值一样
如果不想给后端的接口一一添加权限信息,可以在前端动态获取后端返回的权限集合,直接不在前端显示这个用户不拥有的权限即可
/**
* 根据多条件查询用户的基本信息
* @param vo 多条件的封装类
*/
@ApiImplicitParams({
@ApiImplicitParam(paramType = "body", dataType = "QueryUserVo", name = "vo", value = "多条件的封装类", required = true)
})
@ApiOperation(value = "根据多条件查询用户的基本信息", notes = "根据多条件查询用户的基本信息", httpMethod = "POST")
@PostMapping("/users")
@PreAuthorize("hasAuthority('sys:user:list')")
public R<PageResult<SysUserDomain>>getUserByManyCondition(@RequestBody QueryUserVo vo){
if(StringUtils.isBlank(vo.getPageNum())){
vo.setPageNum("1");
}
if(StringUtils.isBlank(vo.getPageSize())){
vo.setPageSize("20");
}
return userService.getUserByManyCondition(vo);
}