引入依赖
<dependencies>
<!--springsecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
完整流程
UsernamePasswordAuthenticationFilter: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。
认证流程
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
实现步骤
登录
- 定义UserDetailsServiceImpl,实现UserDetailsService接口,返回UserDetails
-
在这个实例中去查询数据库
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户信息 LambdaQueryWrapper<User> queryWrapper=new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(queryWrapper); // 如果没有查询到用户,抛出异常 if (Objects.isNull(user)){ throw new RuntimeException("用户不存在"); } // TODO 查询权限信息 // 返回封装的UserDetails对象 return new LoginUserDetails(user); } } -
创建UserDetails
@Data @NoArgsConstructor @AllArgsConstructor public class LoginUserDetails implements UserDetails { private User user; // TODO 获取权限信息 @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } -
定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new MD5PasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() // session管理 .sessionManagement() //不通过Session获取SecurityContext .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 权限请求管理 .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() // 所有资源都可访问 .antMatchers("/hello").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } } -
创建MD5PasswordEncoder,实现PasswordEncoder,用于md5加密
public class MD5PasswordEncoder implements PasswordEncoder { // 加密 @Override public String encode(CharSequence oldPassword) { return MD5Utils.encrypt(oldPassword.toString()); } // 解密 @Override public boolean matches(CharSequence oldPassword, String encodePassword) { return encodePassword.equals(encode(oldPassword)); } }
2.自定义登录接口
调用ProviderManager的方法进行认证
- 如果认证通过,生成JWT,把用户存入redis:userid作为key,用户信息作为value
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public Result login(User user) {
// AuthenticationManager来用户认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 如果认证失败,给出对应提示
if (Objects.isNull(authentication)) {
throw new RuntimeException("用户名或密码错误,登录失败");
}
// 如果认证通过,使用userId生成一个jwt,存入result返回
LoginUserDetails loginUserDetails = (LoginUserDetails) authentication.getPrincipal();
String userId = String.valueOf(loginUserDetails.getUser().getId());
String jwt = JwtUtils.createJWT(userId);
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
// 把完整的用户信息存入redis,userId为key
redisCache.setCacheObject("login:" + userId, loginUserDetails);
return new Result(200, "登录成功", map);
}
}
校验
- 定义JWT认证过滤器(继承OncePerRequestFilter)
-
获取token
-
解析token获取其中的userid
-
从redis中获取用户信息
-
存入SecurityContextHolder
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 获取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { // 放行 filterChain.doFilter(request, response); return; } // 解析token String userId; try { Claims claims = JwtUtils.parseJWT(token); userId = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } // 从redis中获取用户信息 LoginUserDetails loginUserDetails = redisCache.getCacheObject("token" + userId); if (Objects.isNull(loginUserDetails)){ throw new RuntimeException("用户未登录"); } // 存入SecurityContextHolder // TODO 获取权限消息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken= new UsernamePasswordAuthenticationToken(loginUserDetails, null,null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 放行 filterChain.doFilter(request, response); } }-
过滤器配置,在SecurityConfig类的configure方法里添加
http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);注意:注入JwtAuthenticationTokenFilter
@Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
-
-
退出登录
定义一个登录接口,获取SecurityContextHolder的认证信息,删除redis对应的记录即可
在LoginServiceImpl中
@Override
public Result logout() {
// 获取SecurityContextHolder的认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUserDetails loginUserDetails = (LoginUserDetails) authentication.getPrincipal();
Long userId = loginUserDetails.getUser().getId();
// 删除redis对应的记录
redisCache.deleteObject("login:" + userId);
return new Result(200,"退出登录成功");
}
授权流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
-
设置我们的资源所需要的权限即可。
- 在SecurityConfig类上添加
@EnableGlobalMethodSecurity(prePostEnabled = true)-
在想要添加权限的controller中的方法上添加注解
@PreAuthorize("hasAuthority('system:dept:list')")
-
把当前登录用户的权限信息也存入Authentication。(完成前面的TODO)
-
补全登录,在LoginUserDetails类上添加:
//存储SpringSecurity所需要的权限信息的集合 @JSONField(serialize = false) // 表示不会序列化到redis中 private List<GrantedAuthority> authorities; // TODO 获取权限信息 @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (authorities != null) { return authorities; } // 把permissions的string类型的权限信息封装成SimpleGrantedAuthority authorities = permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities; } -
补全校验,在JwtAuthenticationTokenFilter过滤器类上添加:
// TODO 获取权限消息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(loginUserDetails,null,loginUserDetails.getAuthorities());
-
-
从数据库查找权限信息(PBAC权限模型)
-
MenuMapper类
List<String> selectParamsByUserId(@Param("userId") Long userId); -
MenuMapper.xml
<select id="selectParamsByUserId" resultType="java.lang.String"> SELECT distinct m.perms FROM sys_menu as m INNER JOIN sys_role_menu as rm on m.id = rm.menu_id INNER JOIN sys_user_role as ur on rm.role_id = ur.role_id WHERE ur.user_id = #{userId} AND m.`status` = 0 </select>-
注意:在application.xml完成配置:
mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml
-
-
在UserDetailsServiceImpl类上完成TODO
// TODO 查询权限信息 List<String> list = menuMapper.selectParamsByUserId(user.getId()); // 返回封装的UserDetails对象 return new LoginUserDetails(user,list); -
自定义失败处理
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。
在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
认证过程中出现的异常:会被封装成AuthenticationException,然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
授权过程中出现的异常:会被封装成AccessDeniedException,然后调用AccessDeniedHandler对象的方法去进行异常处理。
Web工具类
public class WebUtils {
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
实现
认证:自定义AuthenticationEntryPoint,实现AuthenticationEntryPoint接口
授权:自定义AccessDeniedHandler,实现AccessDeniedHandler接口
自定义实现类
-
认证
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { Result result = new Result(HttpStatus.FORBIDDEN.value(), "认证识别,请查询用户"); // 转为json格式 String json = JSON.toJSONString(result); WebUtils.renderString(response, json); } } -
授权
@Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { Result result = new Result(HttpStatus.FORBIDDEN.value(), "权限不足"); // 转为json格式 String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }
配置类上添加配置
在SecurityConfig类的configure方法里
// 配置自定义异常处理
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
注意:要先注入对应的处理器
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
解决跨域问题
-
先对SpringBoot配置,运行跨域请求:CorsConfig类中
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 设置允许跨域的路径 registry.addMapping("/**") // 设置允许跨域请求的域名 .allowedOriginPatterns("*") // 是否允许cookie .allowCredentials(true) // 设置允许的请求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 设置允许的header属性 .allowedHeaders("*") // 跨域允许时间 .maxAge(3600); } } -
开启SpringSecurity的跨域访问:在SecurityConfig类的configure方法里
// 开启跨域配置 http.cors();
- 权限控制
权限校验方法
-
hasAuthority:用户符合的权限才可以访问对应资源
@PreAuthorize("hasAuthority('system:dept:list')") @RequestMapping("/hello") public String hello(){ return "hello"; } -
hasAnyAuthority:可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
@PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')") @RequestMapping("/hello") public String hello(){ return "hello"; } -
hasRole:要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。
@PreAuthorize("hasRole('system:dept:list')") @RequestMapping("/hello") public String hello(){ return "hello"; } -
hasAnyRole:有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。
@PreAuthorize("hasAnyRole('admin','system:dept:list')") @RequestMapping("/hello") public String hello(){ return "hello"; }
自定义权限校验方法
-
定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。
@Component("ex") public class SGExpressionRoot { public boolean hasAuthority(String authority){ //获取当前用户的权限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); //判断用户权限集合中是否存在authority return permissions.contains(authority); } } -
在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法
@PreAuthorize("@ex.hasAuthority('system:dept:list')") @RequestMapping("/hello") public String hello(){ return "hello dept"; }
基于配置的权限控制
在SecurityConfig类的configure方法里
http.authorizeRequests()
// 权限配置
.antMatchers("/hello").hasAuthority("system:dept:list")