一 springsecurity 作用
1.1 springsecurity
Spring security是spring家族的一个安全性框架,主要是用来进行用户认证(Authentication)和用户授权(Authorization)的框架。
用户认证:验证用户登录是否合法
用户授权:登录成功后用户具有哪些权限,可以访问哪些资源。
Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter链来进行拦截的。其中最重要的两个过滤器:
UsernamePasswordAuthenticationFilter负责登录认证,FilterSecurityInterceptor负责权限授权。
1.2 认证核心组件
通过 SecurityContext 来获取Authentication,SecurityContext就是我们的上下文对象!这个上下文对象则是交由 SecurityContextHolder 进行管理,你可以在程序任何地方使用它:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityContextHolder原理非常简单,就是使用ThreadLocal来保证一个线程传递同一个对象。
核心组件:
1、Authentication:存储了认证信息,代表当前登录用户
2、SeucirtyContext:上下文对象,用来获取Authentication
3、SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext
Authentication中是什么信息呢:
1、Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象
2、Credentials:用户凭证,一般是密码
3、Authorities:用户权限
1.3 认证流程
1.将最后封装好的认证信息 Authentication,放入:
Authentication authentication = SecurityContextHolder.getContext().setAuthentication();
二 工程搭建
2.1 工程结构
2.2 spring-security搭建
2.2.1 pom文件
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>common-util</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>model</artifactId>
<version>1.0</version>
</dependency>
<!-- Spring Security依赖 -->
<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>
<scope>provided </scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<scope>provided </scope>
</dependency>
说明:依赖包(spring-boot-starter-security)导入后,Spring Security就默认提供了许多功能将整个应用给保护了起来:
1、要求经过身份验证的用户才能与应用程序进行交互
2、创建好了默认登录表单
3、生成用户名为user
的随机密码并打印在控制台上
4、CSRF
攻击防护、Session Fixation
攻击防护
5、等等等等......
2.2.2 添加安全配置类
package com.atguigu.system.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
2.2.3 service-system模块引入spring-security模块
在service-system模块引入spring-security模块,spring-security就对service-system模块的资源进行保护起来。使用spring-security的默认的认证授权保护机制。
2.2.4 访问测试
在浏览器访问:http://localhost:8800/admin/system/sysRole/findAll
自动跳转到了登录页面
密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都会发生变化!
输入用户名,密码,成功访问到controller方法并返回数据,说明Spring Security默认安全保护生效。
在实际开发中,这些默认的配置是不能满足我们需要的,我们需要扩展Spring Security组件,完成自定义配置,实现我们的项目需求。
三 认证的配置
3.1 本案例认证配置流程
Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter
这个过滤器中进行认证的,该过滤器负责认证逻辑。
总结执行顺序:
1.先执行TokenAuthenticationFilter (token验证过滤器),
2.其次执行TokenLoginFilter (认证过滤器),
3.最后执行 UserDetailsServiceImpl(具体模块中查询用户名和密码,权限等信息)
3.1如果验证合法返回到TokenLoginFilter类中success方法中进行后面操作,
3.2如果验证不合法返回到TokenLoginFilter类中unsuccess方法中进行返回提示验证失败
3.2 step1:自定义密码组件
自定义加密组件 实现其passwordEncoder接口
3.3 step2:自定义user类
自定义用户类,需要实现springsecurity框架中包含的User类。
3.4 step3:自定义查询数据库用户的信息
pring security 默认接口是UserDetailsService,里面只有一个方法:loadUserByUsername()
public interface UserDetailsService {
/**
* 根据用户名获取用户对象(获取不到直接抛异常)
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
自定义用户查询用信息实现类,需要实现UserDetailsService接口
2.我们需要自定义自己的业务逻辑实现:通过用户名查询用户信息。写在service-system模块中。
代码:
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if(null == sysUser) {
throw new UsernameNotFoundException("用户名不存在!");
}
if(sysUser.getStatus().intValue() == 0) {
throw new RuntimeException("账号已停用");
}
return new CustomUser(sysUser, Collections.emptyList());
}
}
3.5 step4:自定义认证过滤
登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
1.设置构造函数;2.设置登录认证;3.登录成功后,处理逻辑;3.登录失败后,处理逻辑。
2.代码
/**
* <p>
* 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
* </p>
*
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
public TokenLoginFilter(AuthenticationManager authenticationManager) {
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
//指定登录接口及提交方式,可以指定任意路径
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
}
/**
* 登录认证
* @param req
* @param res
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 登录成功
* @param request
* @param response
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
CustomUser customUser = (CustomUser) auth.getPrincipal();
String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
Map<String, Object> map = new HashMap<>();
map.put("token", token);
ResponseUtil.out(response, Result.ok(map));
}
/**
* 登录失败
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
if(e.getCause() instanceof RuntimeException) {
ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
}
}
}
3.6 step5:响应字符串
2.代码
public class ResponseUtil {
public static void out(HttpServletResponse response, Result r) {
ObjectMapper mapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try {
mapper.writeValue(response.getWriter(), r);
} catch (IOException e) {
e.printStackTrace();
}
}
3.7 step6:自定义 解析token过滤器
因为用户登录状态在token中存储在客户端,所以每次请求接口请求头携带token, 后台通过自定义token过滤器拦截解析token完成认证并填充用户信息实体。
解析验证token过滤器继承于OncePerRequestFilter。验证token是否合法。
代码
package com.atguigu.system.filter;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.utils.JwtHelper;
import com.atguigu.common.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
//认证解析过滤器
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("uri:"+request.getRequestURI());
//如果是登录接口,直接放行
if("/admin/system/index/login".equals(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
if("/prod-api/admin/system/index/login".equals(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthenticationByDefine(request);
if(null != authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
}
}
private UsernamePasswordAuthenticationToken getAuthenticationByDefine(HttpServletRequest request) {
// token置于header里
String token = request.getHeader("token");
logger.info("token:"+token);
if (!StringUtils.isEmpty(token)) {
String useruame = JwtHelper.getUsername(token);
logger.info("useruame:"+useruame);
if (!StringUtils.isEmpty(useruame)) {
String authoritiesString =
(String) redisTemplate.opsForValue().get(useruame);
List<Map> mapList = JSON.parseArray(authoritiesString, Map.class);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Map map : mapList) {
authorities.add(new SimpleGrantedAuthority((String)map.get("authority")));
}
return new UsernamePasswordAuthenticationToken(useruame, null, authorities);
}
}
return null;
}
}
3.8 step7:自定义 webSecurityConfig全局配置类
3.9 测试验证
在浏览器访问:http://localhost:8800/admin/system/sysRole/findAll