SpringSecurity+JWT实现自定义授权 最近学习整合SpringSecurity到分布式框架中,看了几天授权这块实在太难理解,要实现传统的RBAC模型授权还是比较复杂,记录一下。
RBAC RBAC通常有5张表,但是我这里用户和角色是一对一关系,习惯把角色直接放在用户表里面,省去了一张表,看起来也很直观。
文章图片
SpringSecurity配置:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity//里面已经有一个@Configuration注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtTokenFilter jwtTokenFilter;
//校验JWT的拦截器
@Autowired
LoginSuccessHandler loginSuccessHandler;
//登录成功后的操作@Override
protected void configure(HttpSecurity http) throws Exception {
/*
* 校验token是否合法,这个filter会最先进入
*/
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //使用JWT,关闭session
.and()
.httpBasic()
.and()
.authorizeRequests()
.anyRequest().access("@RBACService.hasAccess(request, authentication)")//所有的url需要认证
.and()
.formLogin()
.successHandler(loginSuccessHandler)//登录成功后的操作
// .loginPage("/login")//使用自定义登录页面
.permitAll();
http.formLogin();
http.rememberMe();
//开启记住密码功能,会发送给客户端一个cookie
}//配置密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义认证: SpringSecurity中认证是由UserDetailsService中的loadUserByUsername(String userName)方法校验用户,校验成功会返回一个UserDetails对象,这个UserDetails对象里面就包含了用户的密码权限等信息,其中 Collection extends GrantedAuthority> getAuthorities(); 这个方法就是获取用户的权限集合,我们自己的权限实体类只需要继承GrantedAuthority这个接口,重写getAuthority()方法。getAuthority()方法发挥的是一个字符串,可以理解为权限的别名,我这里为了方便后面判断权限就直接返回权限的url。所以完全自定义关键注意两点:1.自定义的权限实体实现GrantedAuthority接口,重写getAuthority()方法返回权限标识,2.自定义用户的实体类实现UserDetails接口,重写getAuthority()方法返回用户的权限集合
权限实体:
public class Access implements GrantedAuthority, Serializable {
private Integer id;
private String accessName;
private String accessUrl;
private String authority;
//加这个参数为了防止redis序列化转换异常
private Integer parentId;
private Integer menuLevel;
//菜单级别0:方法,1:一级菜单,2:二级,3:三级...
private List children;
@Override
public String getAuthority() { //重写getAuthority()方法返回权限标识
return this.accessUrl;
//这里返回权限的url方便直接和请求的url进行比对
}......省略一大堆getter/setter
}
用户实体:
public class User implements Serializable, UserDetails {
private Integer id;
private String username;
private String password;
private String userPhone;
private Integer roleId;
private List access;
//用户的权限集合private List authorities;
//加这个字段为了防止redis序列化转换异常
boolean accountNonExpired;
boolean accountNonLocked;
boolean credentialsNonExpired;
boolean enabled;
@Override//重写getAuthorities()返回自定义的权限集合
public Collection extends GrantedAuthority> getAuthorities() {
return access;
//Access类已经实现了GrantedAuthority接口并重写了getAuthority()方法
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}......getter/setter......
}
loadUserByUsername()方法执行返回UserDetails才表示认证成功,我这里使用自己的用户实体实现了UserDetails,所以能够直接返回。也可以直接用
org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities(accesses).build();
构建一个UserDetails返回。后面的密码比对就交给SpringSecurity来完成。注:使用了密码编码器后数据库中的密码应该存储加密后的密码@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper usersMapper;
@Autowired
private AccessMapper accessMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = usersMapper.getByUserName(userName);
//查找用户,这里的username是表单输入的用户名
if(user == null){
throw new UsernameNotFoundException("用户不存在");
}
List access = accessMapper.getAccessByUserName(user.getUsername());
user.setAccess(access);
//如果嫌实现接口麻烦可以使用这行构建一个
//UserDetails user= org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities(accesses).build();
return user;
}
}
认证成功:
用户登录成功后根据配置的Security会来到自定义的loginSuccessHandler,一般在这里返回给前端一些用户的权限等信息,同时这里使用了JWT的验证方式,登录成功后需要向客户端返回一个token,下次请求服务器时带上这个token,通过token来校验用户身份信息。这里我用了redis来进行存储token,方便控制用户登录。
import com.colaiven.cola_consumer.config.dto.ResponseMsg;
import com.colaiven.cola_consumer.config.dto.ResultMsg;
import com.colaiven.cola_consumer.model.User;
import com.colaiven.cola_consumer.util.JwtUtils;
import com.colaiven.cola_consumer.util.MenuUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private RedisTemplate,Object> redisTemplate;
/**
* 登录成功执行的操作.
*/
@Override
@Transactional
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth){
try {
User user = (User) auth.getPrincipal();
//auth里面保存了用户的相关信息,getPrincipal()方法会返回一个UserDetails,我这里的User已经实现了UserDetails接口
response.setCharacterEncoding("utf-8");
//设置response编码
response.setContentType("text/html;
charset=utf-8");
String token = JwtUtils.createJWT("",user.getUsername());
//生成token,工具类百度的redisTemplate.opsForValue().set(token,user,30,TimeUnit.MINUTES);
//将token作为key,user对象作为value存在redis,设置失效时间为30分钟
user.setPassword(null);
//置空密码。因为要返回用户信息给客户端response.setHeader("token",token);
//返回token到客户端
Cookie cookie = new Cookie("token", token);
//将token写到cookie方便前端获取
cookie.setPath("/");
response.addCookie(cookie);
ResponseMsg msg = new ResponseMsg(ResultMsg.SUCCESS,user);
PrintWriter writer = response.getWriter();
writer.write(msg.toString());
//返回格式为JSON格式,这里的toString已经设置返回为JSON
writer.flush();
}catch (Exception e) {
e.printStackTrace();
}
}
}
授权 当客户端下一次发送请求时,首先依然是认证,因为我使用jwt去校验用户不在使用session,Security不会自动去帮你核验用户,我们只需要讲登录者的信息保存在SecurityContextHolder上下文中,即表示认证成功。根据我的Security配置首先会来到自定义的JwtTokenFilter,所以首先需要拿到客户端请求时携带的token并解析用户信息,这里的用户信息里面包含了权限。如果客户端未携带token则表示用户未登录。这里我是将用户信息存储到redis中,拿到token后去redis获取用户信息,获得用户的真实权限,再根据客户端请求的url与值比对,成功后生成UsernamePasswordAuthenticationToken对象放到Security上下文中继续后面的授权操作。
import com.colaiven.cola_consumer.model.Access;
import com.colaiven.cola_consumer.model.User;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate,Object> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader("token");
//获取客户端携带的token
if(!StringUtils.isEmpty(token)){
User user = (User) redisTemplate.opsForValue().get(token);
//根据token获取redis中的用户信息
if(user != null){
AbstractAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(user,user.getPassword(),user.getAccess());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
chain.doFilter(request,response);
}
}
根据配置接下来会走到自定义的RBACService,在这里能够拿到request和认证成功后的UserDetails。在这里进行自定义权限核验:
import com.colaiven.cola_consumer.model.Access;
import com.colaiven.cola_consumer.model.User;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class RBACService {
public boolean hasAccess(HttpServletRequest request, Authentication auth) {
try{
User user = (User) auth.getPrincipal();
String url = request.getRequestURI();
for (Access access:user.getAccess()) { //校验权限
if(url.equals(access.getAccessUrl())){
return true;
}
}
return false;
}catch (Exception e){
System.out.println("RBACService:"+e.toString());
return false;
}
}
}
测试 【Spring|SpringSecurity + JWT自定义授权】设置admin角色为1 给该角色分配权限
文章图片
登录成功返回用户的相关信息,token被写入cookie
文章图片
文章图片
带上token请求有权限的/user/getById
文章图片
带上token请求没有权限的/user/getAdmin
文章图片
推荐阅读
- springboot 集成 spring security 自定义登录
- springsecurity|SpringSecurity自定义登录界面
- spring|springboot+mybais+mabatisplus(swagger)实现增删改查接口
- springboot|springboot两种配置文件bootstrap.properties和application.properties的区别
- SpringBoot|SpringBoot读取配置文件到实体类和静态变量
- spring|Spring Cloud微服务治理框架深度解析
- #|【微服务】一文读懂网关概念+Nginx正反向代理+负载均衡+Spring Cloud Gateway(多栗子)
- java|SpringMVC-核心组件
- java|史上最强SpringMVC请求处理流程解析(通俗易懂)