Spring Security
Spring Security
简介
Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性(两方面)的完整解决方案。目前比较主流的用法也是Spring Boot/Spring Cloud + Spring Security
Web 应用的安全性包括:用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是Spring Security重要核心功能。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户(一般采用用户名和密码的形式)
授权:经过认证后判断当前用户是否有权限进行某个操作
快速入门
导入依赖
1
2
3
4
5
6
7
8<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>创建启动类、controller
启动项目,需要输入账号密码(用户名user密码随机生成在日志中)
可以在配置文件中修改账号密码(或者使用配置类):
1
2 admin =
123 =
用户认证
对于用户认证,SpringSecurity提供了多种认证方式
- HTTP BASIC authentication headers:基于IETF RFC 标准。
- HTTP Digest authentication headers:基于IETF RFC 标准。
- HTTP X.509 client certificate exchange:基于IETF RFC 标准。
- LDAP:跨平台身份验证。
- Form-based authentication:基于表单的身份验证。
- Run-as authentication:用户用户临时以某一个身份登录。
- OpenID authentication:去中心化认证。
- Jasig Central Authentication Service:单点登录。
- Automatic “remember-me” authentication:记住我登录(允许一些非敏感操作)。
- Anonymous authentication:匿名登录。
- ….等
基本登录验证
设置配置类
首先进行密码加密配置(在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便)
BCryptPasswordEncoder 就是 PasswordEncoder 接口的实现类
1
2
3
4
5
6
7
8
9
10public interface PasswordEncoder {
//方法用来对明文密码进行加密,返回加密之后的密文。
String encode(CharSequence rawPassword);
//方法是一个密码校对方法,在用户登录的时候,将用户传来的明文密码和数据库中保存的密文密码作为参数,传入到这个方法中去,根据返回的 Boolean 值判断用户密码是否输入正确。
boolean matches(CharSequence rawPassword, String encodedPassword);
//是否还要进行再次加密,这个一般来说就不用了
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
在内存中存入用户:
1
2
3auth.inMemoryAuthentication()
.withUser("admin") //用户名
.password("123").roles("admin"); //密码和角色配置忽略静态资源:
1
2
3
4
5
public void configure(WebSecurity web) throws Exception {
//web.ignoring() 用来配置忽略掉的 URL 地址,一般对于静态文件,我们可以采用此操作
web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
}自定义表单页面(仅适用于前后端不分离):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //任何接口都需要进行认证
.and() //以and进行配置分割
//登录
.formLogin()
.loginPage("/login.html") //所使用的登录页面(该配置即是页面也是接口)
.loginProcessingUrl("/doLogin") //配置登录参数提交接口
.usernameParameter("name") //修改表单参数名
.passwordParameter("passwd") //修改表单参数名
.defaultSuccessUrl("/index") //登录成功跳转接口
.failureForwardUrl("/error") //登录失败跳转接口
.permitAll() //表示登录相关的页面/接口不要被拦截
.and()
//注销
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))
.logoutSuccessUrl("/login")//注销成功跳转的接口
.deleteCookies() //清楚cookie
.clearAuthentication(true) //清除认证信息,默认可以不用配置会自动清楚
.invalidateHttpSession(true) //使 HttpSession 失效,默认可以不用配置会自动清楚
.permitAll()
.and()
.csrf().disable(); //关闭csrf
}注意登录表单的action必须是”/login.html”以及账号和密码输入框的name一个为username一个为password,不然就不到参会报错
登录成功的回调配置有两个:
- defaultSuccessUrl :登录成功后返回刚才页面,可以设置第二个参数true/false,若为true就跟successForwardUrl一样的效果
- successForwardUrl:不管从哪里进入的登录界面,只要登录成功都统一进入指定的接口
- 登录失败的回调配置:
- failureForwardUrl:failureForwardUrl 是forward 跳转 ,failureForwardUrl异常信息存储在request中
- failureUrl :failureUrl 是redirect 跳转,failureUrl认证失败异常信息存储在session中
- 修改注销url的配置:
- logoutUrl:修改默认的注销 URL,默认为GET请求
- logoutRequestMatcher :logoutRequestMatcher 方法不仅可以修改注销 URL,还可以修改请求方式,实际项目中,这个方法和 logoutUrl 任意设置一个即可
前后端分离登录
方式一:
自定义过滤器
因为Spring Security 默认是通过 key/value 的形式来传递登录参数,因为它处理的方式就是 request.getParameter,但是很多时候我们是通过json来传递数据的,就需要自定义
1 | public class LoginFilter extends UsernamePasswordAuthenticationFilter { |
配置过滤器
1 |
|
方式二:
从数据库中获取用户信息
因为需要从数据库中获取用户信息,则不需要InMemoryUserDetailsManager(将用户存入内存数据源中),需要自定义UserDetailsService的实现类
定义UserDetails对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53package com.wht.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class LoginUser implements UserDetails {
private User user;
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
public String getPassword() {
return user.getPassword();
}
public String getUsername() {
return user.getUserName();
}
public boolean isAccountNonExpired() {
return true;
}
public boolean isAccountNonLocked() {
return true;
}
public boolean isCredentialsNonExpired() {
return true;
}
public boolean isEnabled() {
return true;
}
}再定义自定义UserDetailsService的实现类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34package com.wht.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.wht.entity.LoginUser;
import com.wht.entity.User;
import com.wht.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.Objects;
public class UserDetailsServiceImpl implements UserDetailsService {
private UserMapper userMapper;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName,username));
//如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//TODO 根据用户查询权限信息 添加到LoginUser中
//封装成UserDetails对象返回
return new LoginUser(user);
}
}将AuthenticationManager注入进容器:
1
2
3
4
5
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}定义service来处理具体登录逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private AuthenticationManager authenticationManager;
private RedisUtil redisUtil;
public ResponseEntityDemo login(User user) {
//AuthenticationManager authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证没通过,给出对应的提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
//如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userid = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userid);
Map<String,String> map = new HashMap<>();
map.put("token",jwt);
//把完整的用户信息存入redis userid作为key
redisUtil.set("login:"+userid,loginUser);
return ResponseEntityDemo.successWithData(map);
}配置SpringSecurity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}继续配置过滤器将用户封装成Authentication放入SecurityContextHolder中方便后面的认证流程使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60package com.wht.Filter;
import com.wht.entity.LoginUser;
import com.wht.utils.JwtUtil;
import com.wht.utils.RedisUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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.Objects;
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private RedisUtil redisUtil;
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 = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = (LoginUser) redisUtil.get(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}配置过滤器:
1
2//在UsernamePasswordAuthenticationFilter过滤器认证之前拦截
http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);用户注销
只需要提供一个接口,删除掉存入redis中的用户信息即可
controller
1
2
3
4
public ResponseEntityDemo logout(){
return loginServcie.logout();
}service
1
2
3
4
5
6
7
8
9
10
public ResponseEntityDemo logout() {
//获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
//删除redis中的值
redisUtil.del("login:"+userid);
return ResponseEntityDemo.successWithoutData();
}
用户授权
大致流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们只需要做好两步:
- 把当前登录用户的权限信息也存入Authentication。
- 设置我们的资源所需要的权限即可。
授权配置
配置方式
给资源分配权限:
1
2
3
4http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin") //只允许admin进入
.antMatchers("/user/**").hasRole("user") //允许有user角色的进入
.anyRequest().authenticated() //一定要放最后如果要让admin有user权限,可以使用角色继承,使上级可能具备下级的所有权限
1
2
3
4
5
6
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}注意,在配置时,需要给角色手动加上 ROLE_ 前缀。上面的配置表示 ROLE_admin 自动具备 ROLE_user 的权限。
注解方式
添加权限
先开启相关配置:
@EnableGlobalMethodSecurity(prePostEnabled = true)
给接口添加权限限制:
@PreAuthorize(hasAuthority('权限名'))
- hasAuthority:只有有这个权限才能访问
- hasAnyAuthority:只要有其中任一一个权限即可访问
- hasRole:只有有这个角色才能访问(但是会有一个ROLE_前缀)
- hasAnyRole:只要有其中任一一个角色即可访问(同样会有ROLE_前缀)
1
2
3
4
5
public String hello() {
return "hello";
}权限判断可以使用自定义方法:
先定义方法:
1
2
3
4
5
6
7
8
9
10
11
12
public class ExpressionRoot {
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);
}
}在授权时使用:
1
获取权限
给UserDetails添加权限属性(保证登录时将权限存入):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//存储权限信息
private List<String> permissions;
public LoginUser(User user,List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
//存储SpringSecurity所需要的权限信息的集合
private List<GrantedAuthority> authorities;
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
authorities = permissions.stream().
map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}在登录时从数据库查询权限列表封装成loginUser
1
2
3
4
5
6
7
8
9
10
11
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName,username));
//如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//封装成UserDetails对象返回
return new LoginUser(user,menuMapper.selectPermsByUserId(user.getId()));
}在认证过滤时封装进Authentication
1
2UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());统一处理认证与授权异常
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
直接可以在配置类中进行配置(也可以实现接口)
1 | //配置异常处理器 |