简介
Spring Security 是Spring家族中的一个安全管理框架。相比与另外一个安全框架shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
—般Web应用的需要进行认证和授权。
- 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
- 授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
1、初步使用
准备工作
搭建一个简单的SpringBoot工程,在该工程需要引入web服务,其pom.xml如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
创建页面
创建一个HelloController的类,类内容如下:
@RestController
@RequestMapping("hello")
public class HelloController {
@GetMapping
public String hello(){
return "hello world";
}
}
引入Spring Security依赖
在pom.xml中引入Spring Security依赖,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
启动应用,并在浏览器访问:http://localhost:8080/hello的接口时,系统会自动跳转到SpringSecurity默认的登录页面,默认的用户名为: user,密码会输出在控制台,如下:
初始帐号号:user
默认退出接口:/logout
认证原理
SpringSecurity的认证原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,如下图所示:
图中展示了SpringSecurity的核心认证过滤器
UsernamePasswordAuthenticationFilter
:(负责认证)负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter
: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
FilterSecurityInterceptor
:负责权限校验的过滤器。
认证流程
Authentication
: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager
: 定义了认证Authentication的方法
UserDetailsService
: 加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails
: 提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
2、使用JWT自定义认证
思路分析
登录:
1、自定义登录接口,调用ProviderManager的方法进行认证,如果认证通过生成jwt,同时把用户信息存入redis中。
2、自定义UserDetailService:在这个实现类中查询数据库
校验:
1.定义JWT认证过滤器
2、获取Token并解析,获取token中的userid,根据userid从redis中获取用户信息存入SecurityContextHolder中,然后其它的过滤器会从SecurityContextHolder中获取用户信息。
引入JWT依赖
<!-- 引入JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</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.75</version>
</dependency>
配置信息
在application.yml配置文件新增redis连接的相关连接,如下:
spring:
redis:
host: 127.0.0.1
port: 6379
编写工具类
JWT工具类
public class JwtUtil {
//有效期
public static final Long JWT_TTL = 60*60*1000L;
//设置密钥明文
public static final String JWT_KEY = "tech";
public static String getUUID(){
String token = UUID.randomUUID().toString().replace("-", "");
return token;
}
/**
* 生成JWT
* @param subject
* @return
*/
public static String createJWT(String subject){
JwtBuilder builder = getJwtBuilder(subject, JWT_TTL, getUUID());
return builder.compact();
}
/**
* 解析JWT
* @param jwt
* @return
*/
public static Claims parseJWT(String jwt){
SecretKey secretKey = generalKey();
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid){
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis == null){
ttlMillis = JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
JwtBuilder builder = Jwts.builder()
.setId(uuid) // 唯一的ID
.setSubject(subject) // 主题,可以是JSON数据
.setIssuer("techlearn") //签发者
.setIssuedAt(now) //签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);//过期时间
return builder;
}
/**
* 生成加密后的密钥 secretKey
* @return
*/
public static SecretKey generalKey(){
byte[] encodedKey = JWT_KEY.getBytes();
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
}
统一响应类
@Data
public class ResponseResult <T>{
/**
* 状态码
*/
private Integer code;
/**
* 提交信息
*/
private String msg;
/**
* 结果信息
*/
private T data;
public ResponseResult(Integer code, String msg){
this.code = code;
this.msg = msg;
}
public ResponseResult(Integer code, T data){
this.code = code;
this.data = data;
}
public ResponseResult(Integer code, String msg, T data){
this.code = code;
this.msg = msg;
this.data = data;
}
返回Response工具类
public class ResponseUtil {
public static void out(HttpServletResponse response, ResponseResult result){
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
ObjectMapper mapper = new ObjectMapper();
PrintWriter writer = null;
try {
writer = response.getWriter();
mapper.writeValue(writer, result);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(writer != null){
writer.flush();
writer.close();
}
}
}
}
登录用户类
注意,该类需要实现UserDetails类,这样会方便后面的认证授权操作
@Data
public class LoginUser implements UserDetails {
private String uuid;
private String username;
private String password;
// 存储权限信息
private List<String> permissions;
private Set<GrantedAuthority> authorities;
public LoginUser(String username, String password, List<String> permissions){
this.username = username;
this.password = password;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(Objects.nonNull(authorities)){
return authorities;
}
// 把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());;
return authorities;
}
@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自定认证主要有以下几个步骤:
1、创建一个类实现UserDetailsService接口,并重写loadUserByUsername
方法,该方法是根据用户名查询到用户信息,比如用户名,密码,权限信息等。
2、创建访问拦截过滤器,该过滤器需要实现Security中的OncePerRequestFilter
类,在该类中主要对所有访问的进行拦截过滤,并将用户权限之类的信息封将存入SecurityContextHolder
中让SpringSecurity进行认证操作。
3、定义登录认证失败的处理器,该处理器需要实现AuthenticationEntryPoint
接口,该类需要对用户名密码验证失败进行处理,并返回统一的内容供前端使用。
4、定义无权限访问处理器,该处理器需要实现AccessDeniedHandler
接口,该类主要处理对于无访问权限进行处理,并返回统一的内容供前端使用。
5、将2、3、4步自定义的类配置到SpringSecurity认证中,从而替代SpringSecurity中默认的登录认证逻辑。
1、实现UserDetailService接口
创建一个UserServiceImpl
类,该类需要实现UserDetailService
接口,如下:
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO 根据用户名查询用户信息,这里使用假数据进行操作
if(!username.equals("test")){
throw new RuntimeException("用户名错误!!!");
}
String password = passwordEncoder.encode("123456");
// TODO 查询相应的权限信息,这里使用假数据进行测试
List<String> permissions = new ArrayList<>(Arrays.asList("hello", "admin"));
// 将数据封装成UserDetails对象返回
return new LoginUser(username, password, permissions);
}
}
2、定义访问过滤器
定义JwtAuthenticationTokenFilter
过滤器,该过滤器需要继承OncePerRequestFilter
类,记的将JwtAuthenticationTokenFilter
注入到Spring容器中,如下:
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求头中的token
String token = request.getHeader("token");
// token为空则放行
if(!StringUtils.hasText(token)){
System.out.println("token 为空");
// 放行
filterChain.doFilter(request, response);
return;
}
// 解析token
String uuid;
try {
Claims claims = JwtUtil.parseJWT(token);
uuid = claims.getSubject();
}catch (Exception e){
throw new RuntimeException("Token 非法");
}
String redisKey = "login_"+uuid;
LoginUser loginUser = JSON.parseObject((String) redisTemplate.opsForValue().get(redisKey), LoginUser.class);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录!!!");
}
// 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
// 将权限与认证信息存入SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
3、定义认证失败处理器
定义登录认证失败处理器SecurityAuthenticationEntryPoint
,该类需要实现AuthenticationEntryPoint
接口,如下:
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
System.out.println("认证有误");
ResponseUtil.out(response, new ResponseResult(400, authException.getMessage()));
}
}
4、定义权限访问失败处理器
定义权限访问失败处理器AccessDeniedHandlerImpl
,该类处理无访问权限问题,如下:
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
System.out.println("无访问权限");
ResponseUtil.out(response, new ResponseResult<>(500, "无访问权限"));
}
}
5、跨域
浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
新建CorsConfig
配置类,该类需要实现WebMvcConfigurer
接口,如下:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry){
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETED", "PUT", "OPTIONS")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许的时间
.maxAge(3600);
}
}
6、配置自定义至SpringSecurity
自定义一个SecurityConfig
的配置类,该类主要进行SpringSecurity的登录认证,权限等配置,如下:
@Configuration
public class SecurityConfig {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
/**
* 更改SpringSecurity的密码加密方式为BCryptPasswordEncoder
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF 禁用
.csrf().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 过滤请求
.authorizeRequests()
// 对于登录接口,允许匿名访问
.antMatchers("/user/login").anonymous()
// 除了上面外的所有请求全都需要鉴权认证
.anyRequest().authenticated();
//把Token校验过滤器添加到过滤器链中
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity.exceptionHandling()
// 配置认证失败处理器
.authenticationEntryPoint(securityAuthenticationEntryPoint)
// 配置授权访问失败处理器
.accessDeniedHandler(accessDeniedHandler);
// 允许跨域
httpSecurity.cors();
return httpSecurity.build();
}
}
测试
1、新建LoginService
@Service
public class LoginServiceImpl implements LoginService {
@Resource
private AuthenticationManager authenticationManager;
@Autowired
private RedisTemplate redisTemplate;
@Override
public String login(User user) {
// 使用AuthenticationManager的authenticate方法进行用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 如果认证没有通过,给出相应的提示
if(Objects.isNull(authentication)){
throw new RuntimeException("认证失败");
}
// 如果认证通过了,使用UUID(userid)生成JWT
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String uuid = loginUser.getUuid();
String token = JwtUtil.createJWT(uuid);
loginUser.setUuid(uuid);
// 将完整的用户信息存入redis
redisTemplate.opsForValue().set("login_"+uuid, JSON.toJSONString(loginUser));
return token;
}
@Override
public String logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
redisTemplate.delete("login_"+loginUser.getUuid());
return null;
}
}
2、新建LoginController
@RestController
@RequestMapping("user")
public class LoginController {
@Autowired
private LoginService loginService;
@GetMapping("/logout")
public ResponseResult logout(){
loginService.logout();
return new ResponseResult(200, "登出成功");
}
@GetMapping("/login")
public ResponseResult login(){
User user = new User();
user.setUsername("test");
user.setPassword("123456");
String token = loginService.login(user);
return new ResponseResult(200, token);
}
}
启动应用,访问http://localhost:8080/user/login接口,则可以得到token,如下:
{
"code":200,
"msg":"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2YjhiZGEyMTczMTQ0MGQyYTMwN2Y3NzVhZTQ3YzEwNSIsImlzcyI6InRlY2hsZWFybiIsImlhdCI6MTY3MDY4Mjc0NSwiZXhwIjoxNjcwNjg2MzQ1fQ.ZBt1POGx3gy0eyGaHbBuIcmtDYZ4ITvgzEZKkv2eHCk",
"data":null
}
4、新建HelloController
@RestController
@RequestMapping("hello")
public class HelloController {
// 定义访问这个方法需要'hello'权限
@PreAuthorize("hasAnyAuthority('hello')")
@GetMapping("/{id}")
public ResponseResult hello(@PathVariable Integer id) {
return new ResponseResult(200, "hello world");
}
// 定义访问这个方法需要'world'权限
@PreAuthorize("hasAnyAuthority('world')")
@GetMapping("world")
public ResponseResult world(){
return new ResponseResult(200, "这个不需权限即可访问!!!");
}
}
5、开启权限访问
在启动类中,添加EnableGlobalMethodSecurity
注解用来开启注解权限,如下:
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecuritydemoApplication {
public static void main(String[] args) {
SpringApplication.run(SecuritydemoApplication.class, args);
}
}
启动应该,访问:http://localhost:8080/hello,记的需要在header中添加token信息,如下:
原创文章,作者:jiafegn,如若转载,请注明出处:https://www.techlearn.cn/archives/1989