SpringSecurity基本校验使用总结
一、本地视图方式校验(Thymeleaf)
1、基本用户名校验
配置Pom依赖
//父工程 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.4</version> </parent> //依赖 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies>
|
配置application.yaml
spring: application: name: springboot-security-test server: port: 8899
|
配置SpringBootApplication主启动
@SpringBootApplication public class SpingBootApplicationMain { public static void main(String[] args) { SpringApplication.run(SpingBootApplicationMain.class, args); } }
|
编写Controller
@RestController @RequestMapping("/sec") public class TestController { @GetMapping("hello") public String hello(){ return "认证成功"; } }
|
1.1、从配置文件中进行校验
修改配置文件中的application.yaml添加Security配置
security: user: name: byz password: 123
|
启动工程进行验证,访问配置的映射器路径:
进入登陆页面,输入在配置文件中的用户名密码进入
1.2、从配置类中进行校验
1、修改配置文件,注释掉:
# security: # user: # name: byz # password: 123
|
2、新建一个配置类
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder passwordEncoder = (BCryptPasswordEncoder) passwordEncoder(); String password = passwordEncoder.encode("123"); auth.inMemoryAuthentication().withUser("byz").password(password).roles("admin"); } }
|
1.3、从数据库或者其他方式自定义校验UserDetailService
自定义USerDeatilService
实现UserdetailService
接口
@Service("userDetailsService") public class MyUserDetailsServiceImpl implements UserDetailsService {
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUser user = new MyUser(); List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,user,ROLE_admin"); if (user != null) {
return new User("byz", new BCryptPasswordEncoder().encode("123"), grantedAuthorities); } else { throw new UsernameNotFoundException("用户名不存在!"); } } }
|
设置配置类并且重写配置权限方法configure(AuthenticationManagerBuilder auth)
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } }
|
配置类中配置Httpl拦截方法
@Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .loginPage("/user/login") .loginProcessingUrl("/login") .defaultSuccessUrl("/user/success")
.and() .authorizeRequests() .antMatchers("/", "/index", "/user/login").permitAll()
.antMatchers("/role/admin").hasRole("admin") .antMatchers("/role/service").hasAnyRole("user,test") .antMatchers("/role/auth").hasAuthority("admin") .antMatchers("/role/test").hasAnyAuthority("test")
.anyRequest().authenticated()
.and() .csrf().disable()
.exceptionHandling().accessDeniedPage("/unauth");
}
|
2、记住我rememberMe
2.1、记住我(基于数据库)
注入数据源
安全配置类中配置(注入)PersistentTokenRepository
@Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; }
|
配置类中
protected void configure(HttpSecurity http) throws Exception { http .and() .rememberMe().tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60) .userDetailsService(userDetailsService); }
|
页面配置复选框
名称必须叫做remember-me 框架底层设置
<input type="checkbox" name="remember-me"/>
|
3、退出登陆(默认行为)
默认的退出登录URL为/logout
需要自定义就实现LogoutHandler接口
二、分布式Token校验
2.1、编写UserDetail对象(UserDetailService查询会使用到)
编写基础用户对象(存储数据库中)
@Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { @Serial private static final long serialVersionUID = 2211L; private String userName; private String password; private String age; private String birthday; }
|
编写SecurityUser用于SpringSecurity
中权限用户封装
@Data @NoArgsConstructor @AllArgsConstructor public class SecurityUser implements UserDetails {
private transient User currentUserInfo;
private List<String> permissionValueList;
public SecurityUser(User currentUserInfo) { if(currentUserInfo != null){ this.currentUserInfo = currentUserInfo; } }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { Assert.notNull(this.permissionValueList, "Cannot pass a null GrantedAuthority collection"); Set<GrantedAuthority> setAuthorities = new HashSet<>(); for (String grantedAuthority : this.permissionValueList) { if(StringUtils.hasText(grantedAuthority)){ SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(grantedAuthority); setAuthorities.add(simpleGrantedAuthority); } } return setAuthorities; }
@Override public String getPassword() { return null; }
@Override public String getUsername() { return null; }
@Override public boolean isAccountNonExpired() { return false; }
@Override public boolean isAccountNonLocked() { return false; }
@Override public boolean isCredentialsNonExpired() { return false; }
@Override public boolean isEnabled() { return false; } }
|
2.2、编写Security
2.1.1、编写自定义密码加密规则PassWordEncoder
@Component public class DefaultPasswordEncoder implements PasswordEncoder { @Autowired BCryptPasswordEncoder bCryptPasswordEncoder;
public DefaultPasswordEncoder() { }
public DefaultPasswordEncoder(int strength) {
}
@Override public String encode(CharSequence rawPassword) { return bCryptPasswordEncoder.encode(rawPassword); }
@Override public boolean matches(CharSequence rawPassword, String encodedPassword) { String encodeInput = bCryptPasswordEncoder.encode(rawPassword); return encodedPassword.equals(encodeInput); } }
|
2.1.2 Token工具类
- 生成Token(jwt生成—JsonWebToken)
- 根据用户名获取Token
- Token中获取用户名
- 删除Token
Jwt介绍:
JWT头
- alg:表示签名算法
- typ:令牌类型
{ "alg":"HS256", "typ":"JWT" }
|
有效载荷 PayLoad(Claims)
- iss:发行人
- exp:到期时间 Unix 的_时间_
- sub:主题
- aud:用户
- nbf:在此之前不可用
- iat:发布时间
- jti:jwt id 标识一个JWt
- 私有字段
{ "iss":"xxx", "exp":"222", "sub":"loginedUSer", "nbf":"xxx", "aud":"user", "iat":"xxx", "jti":"xxx", "自定义字段":"value" }
|
签名哈希
对上面两部分进行签名,使用指定算法生成前面哈希
例子:
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
|
完整JWt字符串:
拼装结构:
JWTString=Base64(Header).Base64(Payload).签名哈希
|
2.1.2.1、导入JWt依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.4</version> </parent>
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
|
2.1.2.2、编写TokenManager工具类
@Component("tokenManager") public class TokenManager {
private long tokenExpiredTime = 60 * 60 * 1000;
private String tokenSecrect = "111111";
public String createToken(String userName){ String token = Jwts.builder() .setSubject(userName) .setExpiration(new Date(System.currentTimeMillis() + tokenExpiredTime)) .signWith(SignatureAlgorithm.ES512,tokenSecrect) .compressWith(CompressionCodecs.GZIP).compact(); return token; }
public String getUserInfoFromToken(String token){ String userInfo = Jwts.parser() .setSigningKey(tokenSecrect) .parseClaimsJws(token) .getBody() .getSubject(); return userInfo; } public void removeToken(String token){
} }
|
2.1.3 退出登陆处理器(Token)
2.1.3.1、使用Redis存储Token
- 引入Pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
|
- application.yaml中配置Redis
spring: redis: port: 6379 host: localhost
|
2.1.3.2、编写退出处理器
@Component("tokenLogOutHandler") public class TokenLogOutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogOutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) { this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; }
@Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { String token = request.getHeader("token"); if(token!=null){ tokenManager.removeToken(token); String userInfoFromToken = tokenManager.getUserInfoFromToken(token); redisTemplate.delete(userInfoFromToken); } } }
|
退出逻辑配置
@Override protected void configure(HttpSecurity http) throws Exception { http .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(tokenLogOutHandler) .deleteCookies("JSESSIONID") }
|
2.1.4 未授权统一处理AuthenticationEntryPoint
@Component("unAuthenticationEntryPoint") public class UnAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { } }
|
未授权统一权限配置:
@Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(unAuthenticationEntryPoint) }
|
2.3、编写过滤器
2.3.1、认证过滤器
@Component("tokenLoginFilter") public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private RedisTemplate redisTemplate;
private TokenManager tokenManager;
private AuthenticationManager authenticationManager;
public TokenLoginFilter(RedisTemplate redisTemplate, TokenManager tokenManager, AuthenticationManager authenticationManager) { this.redisTemplate = redisTemplate; this.tokenManager = tokenManager; this.authenticationManager = authenticationManager; this.setPostOnly(false); }
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { User user = new ObjectMapper().readValue(request.getInputStream(), User.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword(), new ArrayList<>())); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(); } }
@Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityUser securityUser = (SecurityUser) authResult.getPrincipal(); String token = tokenManager.createToken(securityUser.getCurrentUserInfo().getUserName()); redisTemplate.opsForValue().set(token, securityUser.getPermissionValueList());
}
@Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { } }
|
配置认证过滤器:
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilter(tokenLoginFilter) }
|
2.3.2、编写授权过滤器
@Component("tokenAuthenticationFilter") public class TokenAuthenticationFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); }
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { UsernamePasswordAuthenticationToken authResult = getAuthentication(request); if (authResult != null) { SecurityContextHolder.getContext().setAuthentication(authResult); } chain.doFilter(request, response); }
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String token = request.getHeader("token"); if (token != null) { String username = tokenManager.getUserInfoFromToken(token); List<String> permissionList = (List<String>) redisTemplate.opsForValue().get("username"); List<GrantedAuthority> collect = null; if (permissionList != null) { collect = permissionList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); } return new UsernamePasswordAuthenticationToken(username, token, collect); } return null; }
}
|
配置授权过滤器:
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilter(tokenAuthenticationFilter) }
|
2.4、编写UserDetailService
@Service("userDetailService") public class UserDetailServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new SecurityUser(); } }
|
配置自定义UserDetailService:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); }
|
2.5、编写核心配置类
@Configuration public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UnAuthenticationEntryPoint unAuthenticationEntryPoint;
private final TokenLogOutHandler tokenLogOutHandler;
private final TokenLoginFilter tokenLoginFilter;
private final TokenAuthenticationFilter tokenAuthenticationFilter;
private final UserDetailsService userDetailsService;
@Autowired public TokenWebSecurityConfig(UnAuthenticationEntryPoint unAuthenticationEntryPoint, TokenLogOutHandler tokenLogOutHandler, TokenLoginFilter tokenLoginFilter, TokenAuthenticationFilter tokenAuthenticationFilter,UserDetailsService userDetailsService) { this.unAuthenticationEntryPoint = unAuthenticationEntryPoint; this.tokenLogOutHandler = tokenLogOutHandler; this.tokenLoginFilter = tokenLoginFilter; this.tokenAuthenticationFilter = tokenAuthenticationFilter; this.userDetailsService = userDetailsService; }
@Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(unAuthenticationEntryPoint)
.and() .logout() .logoutUrl("/logout") .addLogoutHandler(tokenLogOutHandler) .deleteCookies("JSESSIONID")
.and() .authorizeRequests()
.anyRequest().authenticated()
.and().csrf().disable()
.addFilter(tokenLoginFilter)
.addFilter(tokenAuthenticationFilter); }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); }
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/"); } }
|
三、Handler配置
3.1、授权成功处理器
1、编写处理器
@Component("authenticationSuccessHandlerImpl") public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { } }
|
2、配置到Security配置文件中
@Override protected void configure(HttpSecurity http) throws Exception { http.successHandler(authenticationSuccessHandlerImpl); }
|
3.2、授权失败处理器
1、编写处理器
@Component("authenticationFailureHandlerImpl") public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
@Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { } }
|
2、配置到Security配置文件中
@Override protected void configure(HttpSecurity http) throws Exception { http.failureHandler(authenticationFailureHandlerImpl); }
|
3.3、退出登陆处理器
1、编写处理器
@Component("logoutSuccessHandlerImpl") public class LogoutSuccessHandlerImpl implements LogoutHandler {
@Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { } }
|
2、配置到Sevurity配置文件中
@Override protected void configure(HttpSecurity http) throws Exception { http .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(logoutSuccessHandlerImpl) .deleteCookies("JSESSIONID") }
|
四、权限注解
4.1、开启权限注解
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { }
|
4.2、常用注解使用
4.2.1、@Secured
适用于方法上,标识当前这个方法是什么样的角色可以访问
@ResponseBody @GetMapping("/securedEnabled") @Secured({"ROLE_test"}) public String securedEnabledTest() { return "允许访问,这里是 @EnableGlobalMethodSecurity(securedEnabled = true) 注解配置使用@Secure"; }
|
4.2.2、@PreAuthorize
适用于方法上,可以用权限或者角色,先校验权限,后执行方法 不通过403
@ResponseBody @GetMapping("/prePostEnabled") @PreAuthorize("hasAnyAuthority('admin')") public String prePostEnabled() { return "允许访问,这里是 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解配置使用 提前校验@PreAuthorize"; }
|
4.2.3、@PostAuthorize
适用于方法上,有任何权限,先执行方法 后校验权限,不通过403
@ResponseBody @GetMapping("/postEnabled") @PostAuthorize("hasAnyAuthority('test')") public String postAuthorize() { return "允许访问,这里是 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解配置使用 后置校验@PostAuthorize"; }
|
4.2.4、@PostFilter
适用于方法上,输出匹配的指定值(留下这个其他过滤)
@ResponseBody @RequestMapping("/postFilter") @PostFilter("filterObject == 'admin'") public List<String> getAllListPostFilter() { ArrayList<String> list = new ArrayList<>(); list.add("admin"); list.add("test"); return list; }
|
会输出 admin , test被过滤掉
4.2.4、@PreFilter
适用于方法上,权限校验之后,输出匹配的指定值
@ResponseBody @RequestMapping("/preFilter") @PreFilter("filterObject == 'test'") public List<String> getAllListPreFilter(@RequestBody List<String> input) { return input; }
|
输入:
[ "test","admin","user","abcd" ]
|
输出:
五、坑
角色配置中不需要增加“ROLE_”前缀,但是注解中需要增加,数据库中不用增加
public ExpressionInterceptUrlRegistry hasRole(String role) { return access(ExpressionUrlAuthorizationConfigurer .hasRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, role)); }
|
this.rolePrefix
在构造方法中进行设置,如果没有覆盖默认的设置GrantedAuthorityDefaults.class
会使用前缀