Spring Boot 集成 Spring Security (安全结构)
我正在参与「启航方案」
本章节将介绍 Spring Boot 集成 Spring Security 5.7(安全结构)。
Spring Boot 2.x 实践案例(代码仓库)
介绍
Spring Security 是一个能够为根据 Spring 的企业运用体系供给声明式的安全拜访操控解决方案的安全结构。
它供给了一组能够在 Spring 运用上下文中装备的 Bean,充分利用了 Spring IOC(操控反转),DI(依靠注入)和 AOP(面向切面编程)功用,为运用体系供给声明式的安全拜访操控功用,减少了为企业体系安全操控编写大量重复代码的作业。
认证和授权作为 Spring Security 安全结构的中心功用:
认证(Authentication):验证当时拜访体系用户是否是本体系用户,而且要确认具体是哪个用户。
授权(Authorization):经过认证后判别当时用户是否具有权限进行某个操作。
快速开始
引进依靠
<!-- Spring Security -->
<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>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<!-- Lombok 插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
装备文件
# 开发环境装备
server:
# 服务端口
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/security?useUnicode=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF8&nullCatalogMeansCurrent=true
username: "root"
password: "88888888"
redis:
host: 127.0.0.1
port: 6379
database: 0
password: 88888888
security:
# 密钥
secret: spring-boot-learning-examples
# 拜访令牌过期时刻(1天)
access-expires: 86400
# 刷新令牌过期时刻(30天)
refresh-expires: 2592000
# 白名单
white-list: /user/login,/user/register,/user/refresh
测验登录
启动项目后,测验拜访某个接口,会主动跳转到 Spring Security 默许登录页面。
默许用户名:user
默许暗码:启动项目时会随机生成暗码并输出在操控台中:
Using generated security password: 0b7bb972-ab4c-461c-ab19-7824d23d9b87
认证
根据数据库加载用户
Spring Security 默许从内存加载用户,需求完成从数据库加载并校验用户。
具体步骤
1)创立 UserServiceImpl 类
2)完成 UserDetailsService 接口
3)重写 loadUserByUsername 办法
4)依据用户名校验用户并查询用户相关权限信息(授权)
5)将数据封装成 UserDetails(创立类并完成该接口) 并回来
中心代码
LoginUser
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LoginUser implements UserDetails {
/**
* 用户编号
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 暗码
*/
@JsonIgnore
private String password;
/**
* 权限调集
*/
@JsonIgnore
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@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;
}
}
留意:需增加
@JsonIgnore
注解,不然会呈现序列化失利问题
UserServiceImpl
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
UserDO user = getUserByUsername(username);
// TODO 查询用户权限信息
return LoginUser.builder()
.id(user.getId())
.username(user.getUsername())
.password(user.getPassword())
.build();
}
@Override
public UserDO getUserByUsername(String username) {
LambdaQueryWrapper<UserDO> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserDO::getUsername, username);
Optional<UserDO> optional = Optional.ofNullable(baseMapper.selectOne(queryWrapper));
return optional.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
}
}
留意:如需测验,需求往用户表中写入数据,而且假如用户暗码想要明文存储,需求在暗码前加
{noop}
暗码加密存储
实际项目中,暗码不会以明文形式存储在数据库中,而 Spring Security 暗码校验器要求数据库中暗码格局为:{id}password
,而默许运用 NoOpPasswordEncoder
加密器,此办法不会对暗码进行加密处理,所以不引荐这种形式。
本项目将运用 Spring Security 供给的 BCryptPasswordEncoder
来进行暗码校验。
具体步骤
1)创立 Spring Security Bean 装备类(防止循环依靠问题)
2)承继 WebSecurityConfigurerAdapter(旧用法)
3)将 BCryptPasswordEncoder 目标注入 Spring 容器中
留意:Spring Security 5.7.x 版别装备办法与以往有所不同,
WebSecurityConfigurerAdapter
在 Spring Security 5.7 版别中已被符号@Deprecated
,未来这个类将被移除,本教程将运用承继WebSecurityConfigurerAdapter
办法来完成 Spring Security 装备,但在实际代码中装备选用最新版别办法!
Spring Security 版别装备区别如下:
1)Spring Boot 2.7.0 版别之前,需求写个装备类承继 WebSecurityConfigurerAdapter
,然后重写 Adapter
中办法进行装备;
2)Spring Boot 2.7.0 版别之后无需再承继 WebSecurityConfigurerAdapter
,只需直接声明装备类,再装备一个生成 SecurityFilterChainBean
办法,把本来 HttpSecurity
装备移动到该办法中即可。
用的挺顺手的 Spring Security 装备类,竟然就要被官方弃用了!
中心代码
@Configuration
public class CommonSecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
留意:同一暗码每次加密后生成密文互不相同,因而需运用 matches() 办法来进行比较。
@SpringBootTest
@Slf4j
class SecurityApplicationTests {
@Test
void passwordEncoder() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123456");
log.info("加密密文:{}", password);
boolean matches = passwordEncoder.matches("123456", password);
log.info("是否匹配:{}", matches);
}
}
登录接口
具体步骤
1)创立 Spring Security 装备类
2)生成 SecurityFilterChain
Bean 办法
3)放行登录接口
4)注入 AuthenticationManager 认证管理器
5)用户认证
6)生成JWT令牌并回来(双令牌机制)
7)拜访令牌(AccessToken)存入 Redis 缓存
留意:Spring Security 5.7.x 版别装备办法与以往有所不同,
WebSecurityConfigurerAdapter
在 Spring Security 5.7 版别中已被符号@Deprecated
,未来这个类将被移除,所以本教程将选用最新版别装备办法!
Spring Security 版别装备区别如下:
1)Spring Boot 2.7.0 版别之前,需求写个装备类承继 WebSecurityConfigurerAdapter
,然后重写 Adapter
中办法进行装备;
2)Spring Boot 2.7.0 版别之后无需再承继 WebSecurityConfigurerAdapter
,只需直接声明装备类,再装备一个生成 SecurityFilterChainBean
办法,把本来 HttpSecurity
装备移动到该办法中即可。
用的挺顺手的 Spring Security 装备类,竟然就要被官方弃用了!
中心代码
1)放行登录接口
需求自定义登陆接口,让 Spring Security 对登录接口放行,之后用户拜访该接口时,不用登录也能拜访:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 过滤恳求
.authorizeRequests()
// 接口放行
.antMatchers("/user/login").permitAll()
// 除上面外的一切恳求悉数需求鉴权认证
.anyRequest()
.authenticated()
.and()
// CSRF禁用
.csrf().disable()
// 禁用HTTP呼应标头
.headers().cacheControl().disable()
.and()
// 根据JWT令牌,无需Session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return httpSecurity.build();
}
}
2)注入 AuthenticationManager 认证管理器
因为在登录接口中,需经过 AuthenticationManager
接口中的 authenticate
办法来进行用户认证,所以需求在 CommonSecurityConfiguration
装备文件中注入 AuthenticationManager
接口。
@Configuration
public class CommonSecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
3)用户认证
调用 AuthenticationManager
接口中的 authenticate
办法来进行用户认证,该办法需传入 Authentication
,因为 Authentication
是接口,因而需求传入它的完成类。
因为登录办法选用账号暗码形式,所以需运用 UsernamePasswordAuthenticationToken
完成类,此类需传入用户名(principal)和暗码(credentials)。
认证成功时,Spring Security 将回来 Authentication
,内容如下:
留意:Authentication 为 NULL 时,说明认证没经过,要么没查询到这个用户,要么暗码比对不经过。
此刻还需生成 JWT 令牌,将其放入呼应中回来,为了能够完成双令牌机制需将拜访令牌存入 Redis 缓存中。
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@Autowired
private UserService userService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseVO<TokenVO> login(@RequestBody @Validated LoginDTO dto) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword()));
UserDO user = userService.getUserByUsername(dto.getUsername());
TokenVO token = JwtUtil.generateTokens(user.getUsername());
redisUtil.set("user:token:" + user.getUsername() + ":string", token.getAccessToken(), JwtUtil.getAccessExpires());
return ResponseVO.success("登录成功", token);
}
}
认证过滤器
具体步骤
1)接口白名单放行
2)从恳求头中解析令牌
3)判别令牌是否存在于黑名单中
4)从 Redis 获取令牌
5)校验令牌是否合法或有效
6)存入 SecurityContextHolder
7)装备过滤器次序
中心代码
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
if (Arrays.stream(JwtUtil.getWhiteList()).anyMatch(uri -> uri.equals(request.getServletPath()))) {
filterChain.doFilter(request, response);
return;
}
String token = JwtUtil.decodeTokenFromRequest(request);
// 判别令牌是否存在黑名单中
if (redisUtil.hasKey("token:black:" + JwtUtil.getJti(token) + ":string")) {
throw new RuntimeException(Code.TOKEN_INVALID.getZhDescription());
}
String username = JwtUtil.getUsername(token);
if (StringUtils.hasText(username) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (!StringUtils.hasText(redisUtil.get("user:token:" + username + ":string"))) {
throw new RuntimeException(Code.ACCESS_TOKEN_EXPIRED.getZhDescription());
}
// 校验令牌是否有效
try {
JwtUtil.decodeAccessToken(token);
JwtUtil.checkTokenValid(token, userDetails.getUsername());
} catch (TokenExpiredException e) {
// TODO 全局异常处理
throw new RuntimeException(Code.ACCESS_TOKEN_EXPIRED.getZhDescription());
} catch (JWTVerificationException e) {
throw new RuntimeException(Code.TOKEN_INVALID.getZhDescription());
}
// 权限信息
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
留意:该过滤器完成接口并不是之前的Filter,而是去承继 OncePerRequestFilter(过滤器抽象类),通常被用于承继完成并在每次恳求时只履行一次过滤)。
在装备文件中,将过滤器加到 UsernamePasswordAuthenticationFilter 前面:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 过滤恳求
.authorizeRequests()
// 静态资源放行
.antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
// 接口放行
.antMatchers(JwtUtil.getWhiteList()).permitAll()
// 除上面外的一切恳求悉数需求鉴权认证
.anyRequest()
.authenticated()
.and()
// CSRF禁用
.csrf().disable()
// 禁用HTTP呼应标头
.headers().cacheControl().disable()
.and()
// 根据JWT令牌,无需Session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 拦截器
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}
退出登录
JWT最大优势在于它是无状况
,本身包含了认证鉴权所需求的一切信息,服务器端无需对其存储,从而给服务器减少了存储开支。
可是无状况引出的问题也是可想而知的,它无法作废未过期的JWT。举例说明注销场景下,就传统的cookie/session
认证机制,只需求把存在服务器端的session删掉就OK了。
可是JWT呢,它是不存在服务器端的啊,好的那我删存在客户端的JWT行了吧。额,社会本就复杂别再诈骗自己了好么,被你在客户端删掉的JWT仍是能够经过服务器端认证的。
运用JWT要十分明确一点:JWT失效仅有途径就是等待时刻过期
。
本教程借助黑名单方案完成JWT失效:
退出登录时,将拜访令牌放入 Redis 缓存中,而且设置过期时刻为拜访令牌过期时刻;恳求资源时判别该令牌是否在 Redis 中,假如存在则回绝拜访。
具体步骤
1)全局过滤器中需求判别黑名单是否存在当时拜访令牌
2)解析恳求头中令牌(JTI
与 EXPIRES_AT
)
3)将JTI字段作为键存放到 Redis 缓存中,并设置拜访令牌过期时刻
4)清除认证信息
5)装备退出登录接口与处理器
实战!退出登录时如何借助外力使JWT令牌失效?
中心代码
@Service
@RequiredArgsConstructor
public class LogoutHandler implements org.springframework.security.web.authentication.logout.LogoutHandler {
@Autowired
private RedisUtil redisUtil;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = JwtUtil.decodeTokenFromRequest(request);
blacklist(token);
SecurityContextHolder.clearContext();
}
/**
* 参加黑名单
*
* @param token 令牌
*/
private void blacklist(String token) {
String jti = JwtUtil.getJti(token);
Long expires = JwtUtil.getExpires(token);
redisUtil.set("token:black:" + jti + ":string", StringConstant.EMPTY, DateUtil.minusSeconds(expires));
}
}
退出登录成功处理器:
@Component
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
SecurityContextHolder.clearContext();
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.OK.value());
response.getWriter().println(GenericJacksonUtil.objectToJson(ResponseVO.success()));
response.getWriter().flush();
}
}
在 Spring Security 装备文件中装备退出登录接口与处理器:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private LogoutHandler logoutHandler;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 过滤恳求
.authorizeRequests()
// 静态资源放行
.antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
// 接口放行
.antMatchers(JwtUtil.getWhiteList()).permitAll()
// 除上面外的一切恳求悉数需求鉴权认证
.anyRequest()
.authenticated()
.and()
// CSRF禁用
.csrf().disable()
// 禁用HTTP呼应标头
.headers().cacheControl().disable()
.and()
// 根据JWT令牌,无需Session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 拦截器
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
// 退出登录
.logout()
.logoutUrl("/user/logout")
.logoutSuccessHandler(logoutSuccessHandler)
.addLogoutHandler(logoutHandler);
return httpSecurity.build();
}
}
授权
在 Spring Security 中,会运用 FilterSecurityInterceptor(默许) 来进行权限校验。
在 FilterSecurityInterceptor 中会从 SecurityContextHolder 获取其间的 Authentication,Authentication 包含权限信息,用来判别当时用户是否具有拜访当时资源所需的权限。
具体步骤
1)敞开权限注解
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}
敞开之后,在需求权限才干拜访的接口上打上 @PreAuthorize 注解即可。
2)查询用户权限信息(见中心代码)
3)封装权限信息
重写 loadUserByUsername 办法时,查询出用户后,还需将用户对应的权限信息,封装到之前定义的 UserDetails 的完成类 LoginUser 并回来:
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LoginUser implements UserDetails {
/**
* 用户编号
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 暗码
*/
private String password;
/**
* 菜单调集
*/
private List<MenuDO> menuList;
/**
* 权限调集
*/
@JsonIgnore
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (Objects.nonNull(authorities)) {
return authorities;
}
return menuList.stream()
.filter(menu -> StringUtils.hasText(menu.getPermission()))
.map(menu -> new SimpleGrantedAuthority(menu.getPermission()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return password;
}
@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;
}
}
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
UserDO user = getUserByUsername(username);
// 查询用户菜单列表
List<MenuDO> menuList = listUserPermissions(user.getId());
// 查询用户权限信息
return new LoginUser(user, menuList);
}
}
中心代码
<select id="listMenusByRoleIds" resultType="com.starimmortal.security.pojo.MenuDO">
SELECT t1.id, t1.parent_id, t1.`name`, t1.`path`, t1.permission, t1.`icon`, t1.component, t1.`type`, t1.`visible`, t1.`status`, t1.keep_alive, t1.sort_order, t1.create_time, t1.update_time, t1.is_deleted
FROM `sys_menu` AS t1
JOIN sys_role_menu AS t2 ON t1.id = t2.menu_id
WHERE t1.is_deleted = 0 AND t1.`status` = 0
AND t2.role_id IN
<foreach collection="roleIds" item="roleId" index="index" open="(" separator="," close=")">
#{roleId}
</foreach>
GROUP BY t1.id
</select>
权限操控
根据办法注解
Spring Security 默许是封闭办法注解,敞开它只需求经过引进 @EnableGlobalMethodSecurity
注解即可:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}
@EnableGlobalMethodSecurity
供给了以下三种办法:
1)prePostEnabled:根据表达式(Spring EL)注解:
-
@PreAuthorize:进入办法之前验证授权:
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
表明办法履行之前,判别办法参数值是否等于
principal
中保存的参数值;或者当时用户是否具有ROLE_ADMIN
权限,两者符合其间一种即可拜访该办法,内置如下办法:-
hasAuthority:只能传入一个权限,只要用户有这个权限才干够拜访资源;
-
hasAnyAuthority:能够传入多个权限,只要用户有其间恣意一个权限都能够拜访对应资源;
-
hasRole:要求有对应人物才干够拜访,可是它内部会把传入的参数拼接上 ROLE_ 后再去比较:
@PreAuthorize("hasRole('system:dept:list')")
留意:用户有
system:dept:list
权限是无法拜访的,得有ROLE_system:dept:list
权限才干够。 -
hasAnyRole:有恣意人物即可拜访。
-
-
@PostAuthorize:检查授权办法之后才被履行而且能够影响履行办法的回来值:
@PostAuthorize("returnObject.username == authentication.principal.nickName") public CustomUser loadUserDetail(String username) { return userRoleRepository.loadUserByUserName(username); }
-
@PostFilter:在办法履行之后履行,而且这儿能够调用办法的回来值,然后对回来值进行过滤或处理或修正并回来。
-
@PreFilter:在办法履行之前履行,而且这儿能够调用办法的参数,然后对参数值进行过滤或处理或修正。
2)securedEnabled:敞开根据人物注解:
@Secured("ROLE_VIEWER")
public String getUsername() {}
@Secured({ "ROLE_DBA", "ROLE_ADMIN" })
public String getNickname() {}
@Secured(“ROLE_VIEWER”):只要具有 ROLE_VIEWER
人物的用户,才干够拜访;
@Secured({ “ROLE_DBA”, “ROLE_ADMIN” }):具有 "ROLE_DBA", "ROLE_ADMIN"
两个人物中的恣意一个人物,均可拜访。
留意:@Secured 注解不支持 Spring EL 表达式!
3)jsr250Enabled:敞开对JSR250注解:
-
@DenyAll:回绝一切权限
-
@RolesAllowed:在功用及运用办法上与
@Secured
完全相同 -
@PermitAll:承受一切权限
根据装备文件
办法称号 | 办法作用 |
---|---|
permitAll() |
表明所匹配的URL任何人都答应拜访 |
anonymous() |
表明能够匿名拜访匹配的URL。和permitAll() 作用相似,仅仅设置为anonymous() 的url会履行filterChain 中的filter |
denyAll() |
表明所匹配的URL都不答应被拜访。 |
authenticated() |
表明所匹配的URL都需求被认证才干拜访 |
rememberMe() |
答应经过remember-me登录的用户拜访 |
access() |
SpringEl 表达式成果为true时能够拜访 |
fullyAuthenticated() |
用户完全认证能够拜访(非remember-me下主动登录) |
hasRole() |
假如有参数,参数表明人物,则其人物能够拜访 |
hasAnyRole() |
假如有参数,参数表明人物,则其间任何一个人物能够拜访 |
hasAuthority() |
假如有参数,参数表明权限,则其权限能够拜访 |
hasAnyAuthority() |
假如有参数,参数表明权限,则其间任何一个权限能够拜访 |
hasIpAddress() |
假如有参数,参数表明IP 地址,假如用户IP 和参数匹配,则能够拜访 |
自定义异常处理
在 Spring Security 中,认证或者授权的过程中呈现异常会被 ExceptionTranslationFilter 捕获,在 ExceptionTranslationFilter 中会去判别是认证失利仍是授权失利呈现的异常。
1)自定义认证失利异常
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().println(GenericJacksonUtil.objectToJson(ResponseVO.error(Code.UN_AUTHORIZATION.getCode(), Code.UN_AUTHORIZATION.getZhDescription(), authException.getMessage())));
response.getWriter().flush();
}
}
2)自定义授权失利异常
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().println(GenericJacksonUtil.objectToJson(ResponseVO.error(Code.UN_AUTHENTICATION.getCode(), Code.UN_AUTHENTICATION.getZhDescription(), accessDeniedException.getMessage())));
response.getWriter().flush();
}
}
3)Spring Security 装备
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private RestAccessDeniedHandler restAccessDeniedHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 过滤恳求
.authorizeRequests()
// 静态资源放行
.antMatchers(STATIC_RESOURCE_WHITE_LIST).permitAll()
// 接口放行
.antMatchers(JwtUtil.getWhiteList()).permitAll()
// 除上面外的一切恳求悉数需求鉴权认证
.anyRequest()
.authenticated()
.and()
// CSRF禁用
.csrf().disable()
// 禁用HTTP呼应标头
.headers().cacheControl().disable()
.and()
// 根据JWT令牌,无需Session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 认证与授权失利处理类
.exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint)
.accessDeniedHandler(restAccessDeniedHandler)
.and()
// 拦截器
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}
跨域
浏览器出于安全考虑,运用 XMLHttpRequest 目标发起 HTTP 恳求时必须恪守同源战略,不然就是跨域的 HTTP 恳求,默许情 况下是被制止的,同源战略要求源相同才干正常进行通讯,即协议、域名、端口号都完全一致。
1)Spring Boot 跨域装备
@Configuration(proxyBeanMethods = false)
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
2)Spring Security 跨域装备
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 跨域
.cors()
.and()
.headers().frameOptions().disable();
return httpSecurity.build();
}
}