认证授权
认证授权
1.完整流程
SpringSecurity的原理起始就是一个过滤器链,内部包含了各种功能的过滤器
- WebAsyncManagerIntegrationFilter:将WebAsyncManger与SpringSecurity上下文进行集成
- SecurityContextPersistenceFilter:在处理请求之前, 将安全信息加载到SecurityContextHolder中
- HeaderWriterFilter:处理头信息假如响应中
- CsrfFilter:处理CSRF攻击
- LogoutFilter:处理注销登录
- UsernamePasswordAuthenticationFilter,处理表单登录
- DefaultLoginPageGeneratingFilter:配置默认登录页面
- DefaultLogoutPageGeneratingFilter:配置默认注销页面
- BasicAuthenticationFilter:处理HttpBasic登录
- RequestCacheAwareFilter:处理请求缓存
- SecurityContextHolderAwareRequestFilter:包装原始请求
- AnonymousAuthenticationFilter:配置匿名认证
- SessionManagementFilter:处理session并发问题
- ExceptionTranslationFilter,异常处理
- FilterSecurityInterceptor,授权

自己实现对SpringSecurity的流程做修改(登陆)

请求其他方法(认证)

思路分析
登陆:
- 自定义登陆接口
- 调用ProviderManager的方法进行认证
- 自定义UserDetailsService
- 实现中去查询数据库
校验:
- 定义JWT认证过滤器
- 获取token
- 解析token,从(缓存)获取用户信息
2.maven依赖
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.用户登录
自定义登陆接口,让SpringSecurity对这个接口放行,让用户访问这个接口时不用登陆也能访问
在接口中通过AuthenticationManager的authenticate方法来进行用户认证,需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话生成一个jwt,放入相应返回,并且为了让用户下次请求时能通过jwt识别出具体是哪个用户,需要把用户信息存入redis,可以把用户id作为key
3.1自定义UserDetailsService
自定义UserDetailsService,实现去数据库查询用户信息
- 实现UserDetailsService接口
- 重写loadUserByUsername方法
import javax.annotation.Resource;
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.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.game.server.bean.SecurityUser;
import com.game.server.bean.UserInfo;
import com.game.server.exception.ReturnCode200Exception;
import com.game.server.mapper.UserInfoMapper;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Resource
private UserInfoMapper userInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<UserInfo> q = new LambdaQueryWrapper<>();
UserInfo userInfo = userInfoMapper.selectOne(q.eq(UserInfo::getUsername, username));
if (userInfo == null) {
throw new ReturnCode200Exception("用户名或者密码错误");
}
// TODO 查询对应的权限信息
//把数据封装成UserDetail类返回
return new SecurityUser(userInfo);
}
}
注意
这里需要返回一个UserDetails对象,我们可以自定义一个实体类
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class SecurityUser implements UserDetails {
private static final long serialVersionUID = 803802923747104863L;
private UserInfo userInfo;
private List<String> permissions;
private List<SimpleGrantedAuthority> authorities;
public SecurityUser(UserInfo userInfo, List<String> permissions) {
super();
this.userInfo = userInfo;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 将permissions中的权限信息进行封装
if (authorities != null) {
return authorities;
}
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return userInfo.getPassword();
}
@Override
public String getUsername() {
return userInfo.getPassword();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3.2密码加密方式
通过BCryptPasswordEncoder方式进行密码加密,将PasswordEncoder注入spring容器管理
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 创建BCryptPasswordEncoder,注入容器,密码加密方式
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
测试
public static void main(String[] args) {
PasswordEncoder passwordEncoder1 = new BCryptPasswordEncoder();
System.out.println(passwordEncoder1.encode("1234"));
boolean matches = passwordEncoder1.matches("1234",
"$2a$10$qh2ybkFoWPngVD1uk66FkOPCRKsb1rCk7ff75g2.XHFRBTkkL9klq");
System.out.println(matches);
}
3.3用户登陆
- 自定义登陆接口
- 调用ProviderManager的方法进行认证
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCacheUtil redisCacheUtil;
@Override
public ReturnResult login(UserInfo userInfo) {
// 进行用户认证
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userInfo.getUsername(), userInfo.getPassword());
Authentication authenticate = null;
try {
authenticate = authenticationManager.authenticate(authentication);
} catch (AuthenticationException e) {
throw new ReturnCode200Exception(e.getMessage());
}
SecurityUser securityUser = (SecurityUser) authenticate.getPrincipal();
String id = securityUser.getUserInfo().getId();
// 生成jwt-token,存储redis
String token = JwtUtils.generateStringTokenExpireInMinutes(id, 60 * 12);
securityUser.setAuthorities(null);
redisCacheUtil.hmset("login-" + id, BeanMapUtils.entityToMapObject(securityUser));
Map<String, String> res = new HashMap<>();
res.put("toekn", token);
return ReturnResultSuccess.builder().data(res).build();
}
}
警告
上面需要从spring容器中拿到AuthenticationManager对象,通过authenticate进行用户认证
- 需要将AuthenticationManager注入spring容器
- 需要将登陆等接口进行过滤
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPointImpl;
@Autowired
private AccessDeniedHandler accessDeniedHandlerImpl;
/**
* 创建BCryptPasswordEncoder,注入容器,密码加密方式
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 将AuthenticationManager注入容器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()// 关闭csrf
// 不通过session获取securityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 对于登陆接口,允许匿名访问
.authorizeRequests().antMatchers("/user/login", "user/register")
// 除上面接口外所有请求全部需要鉴权
.anonymous().anyRequest().authenticated();
// 添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加异常处理器
http.exceptionHandling()
// 认证失败处理器
.authenticationEntryPoint(authenticationEntryPointImpl)
// 授权失败处理器
.accessDeniedHandler(accessDeniedHandlerImpl);
// 允许跨域
http.cors();
}
}
4.token认证过滤器
- 定义JWT认证过滤器
- 获取token
- 解析token,从(缓存)获取用户信息
4.1代码实现
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCacheUtil redisCacheUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
filterChain.doFilter(request, response);
return;
}
// 解析token
String id = "";
try {
Payload<Object> idFromToken = JwtUtils.getIdFromToken(token);
id = idFromToken.getId();
} catch (ExpiredJwtException e) {
e.printStackTrace();
throw new ReturnCode200Exception("token 非法");
}
// 从redis中获取用户信息
String redisKey = "login-" + id;
Map<Object, Object> user = redisCacheUtil.hmget(redisKey);
if (user == null) {
throw new ReturnCode200Exception("用户未登陆");
}
SecurityUser userInfo = BeanMapUtils.mapToEntity(user, SecurityUser.class);
// 存入SecurityContextHolder
// 获取权限信息封装到authentication
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userInfo, null,
null);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
4.2配置认证
将过滤器放在UsernamePasswordAuthenticationFilter过滤器之前
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 创建BCryptPasswordEncoder,注入容器,密码加密方式
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 将AuthenticationManager注入容器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()// 关闭csrf
// 不通过session获取securityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 对于登陆接口,允许匿名访问
.authorizeRequests().antMatchers("/user/login", "user/register")
// 除上面接口外所有请求全部需要鉴权
.anonymous().anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
5.用户登出
用户登出比较简单,前端携带token进行用户登出即可
@Override
@OperationLogger
public ReturnResult logout() {
// 获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder
.getContext().getAuthentication();
UserInfo userInfo = (UserInfo) authentication.getPrincipal();
String id = userInfo.getId();
// 清除redis信息
redisCacheUtil.del("login-" + id);
return ReturnResultSuccess.builder().build();
}
6.授权基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息,当前用户是否拥有访问当前资源所需的权限。
所以需要把当前用户的权限信息存入Authentication中,然后设置资源所需权限即可
6.1修改UserDetails实体类
在原有的实体类上增加成员变量,并修改权限方法
private List<String> permissions;
private List<SimpleGrantedAuthority> authorities;
public SecurityUser(UserInfo userInfo, List<String> permissions) {
super();
this.userInfo = userInfo;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 将permissions中的权限信息进行封装
if (authorities != null) {
return authorities;
}
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
6.2在UserDetailsService实现类中赋值权限
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Resource
private UserInfoMapper userInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<UserInfo> q = new LambdaQueryWrapper<>();
UserInfo userInfo = userInfoMapper.selectOne(q.eq(UserInfo::getUsername, username));
if (userInfo == null) {
throw new ReturnCode200Exception("用户名或者密码错误");
}
// 查询对应的权限信息
List<String> permissions = userInfoMapper.selectPermByUserId(userInfo.getId());
return new SecurityUser(userInfo, permissions);
}
}
6.3修改SecurityConfig配置类
增加@EnableGlobalMethodSecurity(prePostEnabled = true)
注解
6.4修改JwtAuthenticationTokenFilter过滤器添加权限
// 获取权限信息封装到authentication
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userInfo, null,
userInfo.getAuthorities());
7.自定义失败处理
在SpringSecurity中,如果我们认证或者授权过程中出现了异常会被ExceptionTranslationFilter捕获到,在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象去进行异常处理
所以自定义异常处理,只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
ReturnResult re = ReturnResultError.builder().msg("用户授权失败,请联系管理员查看权限信息").build();
String json = JSON.toJSONString(re);
response.setStatus(ConstantCommon.RETURN_CODE_200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(json);
}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ReturnResult re = ReturnResultError.builder().msg("用户认证失败,请登陆").build();
String json = JSON.toJSONString(re);
response.setStatus(ConstantCommon.RETURN_CODE_200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(json);
}
}
修改配置文件
@Autowired
private AuthenticationEntryPoint authenticationEntryPointImpl;
@Autowired
private AccessDeniedHandler accessDeniedHandlerImpl;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()// 关闭csrf
// 不通过session获取securityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 对于登陆接口,允许匿名访问
.authorizeRequests().antMatchers("/user/login", "user/register")
// 除上面接口外所有请求全部需要鉴权
.anonymous().anyRequest().authenticated();
// 添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加异常处理器
http.exceptionHandling()
// 认证失败处理器
.authenticationEntryPoint(authenticationEntryPointImpl)
// 授权失败处理器
.accessDeniedHandler(accessDeniedHandlerImpl);
}
8.跨域处理
8.1设置SpringBoot允许跨域
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域路径
registry.addMapping("/**")
// 设置允许跨域请求域名
.allowedOrigins("*")
// 设置允许cookie
.allowCredentials(true)
// 设置允许请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 是只允许header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
8.2设置SpringSecurity允许跨域
在配置文件中增加http.cors();