5.5 Spring Security
5.5.1 引入spring security
- 在common模块下的spring-security模块中引入依赖:
<!-- 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>
- 在spring-security模块中添加配置类:
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注解开启spring security的默认行为
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
- 在service-oa模块中引入spring-security模块:
<dependency>
<groupId>org.example</groupId>
<artifactId>spring-security</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 启动项目测试,直接访问后端接口就会进入认证页面,默认用户名是user,密码在控制台中。
5.5.2 spring security工作流程
/**
* 用户认证和授权过程:
* 1.用户输入用户名和密码
* 2.由UsernamePasswordAuthenticationFilter过滤器
* 通过attemptAuthentication方法将用户名和密码封装成UsernamePasswordAuthenticationToken对象
* UsernamePasswordAuthenticationToken authRequest
* = new UsernamePasswordAuthenticationToken(username, password);
* 3.接着调用getAuthenticationManager().authenticate(authRequest)方法进行认证,并返回Authentication对象
* return this.getAuthenticationManager().authenticate(authRequest);
* 调用的authenticate方法是AuthenticationManager接口的实现类ProviderManager中的方法
* 4.在ProviderManager的authenticate方法中,调用AuthenticationProvider接口的
* 抽象类AbstractUserDetailsAuthenticationProvider中的authenticate方法
* result = provider.authenticate(authentication);
* 5.provider.authenticate方法中调用了retrieveUser方法,并在retrieveUser方法中,调用了UserDetailsService的
* loadUserByName方法,用来根据输入的用户名查询数据库或配置文件等获取密码
* user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
* return this.delegate.loadUserByUsername(username);
* 6.返回查询的用户信息给AbstractUserDetailsAuthenticationProvider,并调用DaoAuthenticationProvider实现类的
* additionalAuthenticationChecks方法,通过passwordEncoder验证查询的密码与输入密码是否一致
* if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
* this.logger.debug("Authentication failed: password does not match stored value");
* throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
* }
* 7.如果密码验证成功,则调用createSuccessAuthentication方法封装Authentication对象并返回,再将其存储在上下文对象中
* SecurityContextHolder.getContext().setAuthentication(authResult);
*
*/
5.5.3 项目中引入spring security
之前分析过程的时候可以发现,要实现我们自己的验证逻辑,起码有三个接口需要实现:passwordEncoder、UserDetails和UserDetailsService,这三个组件用于验证环节。
5.5.3.1 UserDetails
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import pers.beiluo.yunshangoffice.model.system.SysUser;
import java.util.Collection;
/**
* 实现UserDetails需要重写很多方法,所以可以直接继承org.springframework.security.core.userdetails.User类
* 这个类已经实现了UserDetails接口中的一些方法
*/
public class CustomizedUser extends User {
private SysUser sysUser;
public CustomizedUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomizedUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
public SysUser getSysUser() {
return sysUser;
}
public void setSysUser(SysUser sysUser) {
this.sysUser = sysUser;
}
}
5.5.3.2 UserDetailsService
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public interface UserDetailsService extends UserDetailsService{
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
//继承org.springframework.security.core.userdetails.UserDetailsService,用于组件自动注入,否则spring不会管理我们自定义的组件
@Service
public class UserDetailsServiceImpl implements org.springframework.security.core.userdetails.UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
/**
* 这个方法从数据库中根据用户名查询用户
* @param username 用户名
* @return 用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户
LambdaQueryWrapper<SysUser> sysUserLambdaQueryWrapper = new LambdaQueryWrapper<>();
sysUserLambdaQueryWrapper.eq(SysUser::getUsername,username);
SysUser sysUser = sysUserMapper.selectOne(sysUserLambdaQueryWrapper);
if(null == sysUser){
throw new UsernameNotFoundException("用户不存在");
}
if(sysUser.getStatus() == 0){
throw new CustomizedException(201,"用户已停用");
}
return new CustomizedUser(sysUser, Collections.emptyList());
}
}
5.5.3.3 PasswordEncoder
public class CustomizedPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return MD5.encrypt(charSequence.toString());
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}
5.5.3.4 UsernamePasswordAuthenticationFilter
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public MyUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager){
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(
new AntPathRequestMatcher("/admin/system/index/login","POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
//JSON转对象
LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class);
//封装用户名和密码
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
= new UsernamePasswordAuthenticationToken(loginVo.getUsername(), MD5.encrypt(loginVo.getPassword()));
return this.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 登录成功执行
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain,Authentication authResult) {
CustomizedUser principal = (CustomizedUser) authResult.getPrincipal();
//创建token并返回
String token
= JWTHelper.createToken(principal.getSysUser().getId(), principal.getSysUser().getUsername());
HashMap<String, Object> stringObjectHashMap = new HashMap<>();
stringObjectHashMap.put("token",token);
//将map作为响应体返回
ResponseUtil.resultResponse(response, Result.ok(stringObjectHashMap));
}
/**
* 失败时执行
*/
protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,
AuthenticationException failed){
ResponseUtil.resultResponse(response,Result.build(null, ResultCodeEnum.LOGIN_FAIL));
}
}
5.5.3.5 OncePerRequestFilter
//这个类用于token验证,如果请求报文含有token,就把token封装起来存储在上下文对象中
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
//如果请求的是登录页面,则直接放行
if("/admin/system/index/login".equals(request.getRequestURI())){
chain.doFilter(request,response);
return;
}
//通过getAuthentication方法,将token封装在UsernamePasswordAuthenticationToken对象中
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if(null != authentication){
//如果token不为空,则将token存储在上下文对象中
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request,response);
}else{
ResponseUtil.resultResponse(response,Result.build(null,ResultCodeEnum.LOGIN_FAIL));
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
String token = request.getHeader("token");
if(!StringUtils.isEmpty(token)){
return new UsernamePasswordAuthenticationToken(JWTHelper.getUsername(token),null, Collections.emptyList());
}
return null;
}
}
5.5.3.6 ResponseUtil
由于过滤器不能自动返回响应报文,所以需要手写一个工具方法用于响应报文。
public class ResponseUtil {
public static void resultResponse(HttpServletResponse response, Result ret){
ObjectMapper mapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try {
mapper.writeValue(response.getWriter(), ret);
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.5.4 配置类
@Configuration
//@EnableWebSecurity注解开启spring security的默认行为
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomizedPasswordEncoder customizedPasswordEncoder;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http
//关闭csrf
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
//.antMatchers("/admin/system/index/login").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
.addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilter(new MyUsernamePasswordAuthenticationFilter(authenticationManager()));
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService和加密器
auth.userDetailsService(userDetailsService).passwordEncoder(customizedPasswordEncoder);
}
/**
* 配置哪些请求不拦截
* 排除swagger相关请求
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
}
}
5.6 用户授权
在实现了认证后,需要对方法访问进行更细的权限划分,下面实现用户授权功能。
5.6.1 修改loadUserByUsername
UserDetails中有一个属性是存放权限的集合,所以我们在loadUserByUsername方法中查询用户信息时,需要同时查询用户权限并返回。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户
LambdaQueryWrapper<SysUser> sysUserLambdaQueryWrapper = new LambdaQueryWrapper<>();
sysUserLambdaQueryWrapper.eq(SysUser::getUsername,username);
SysUser sysUser = sysUserMapper.selectOne(sysUserLambdaQueryWrapper);
if(null == sysUser){
throw new UsernameNotFoundException("用户不存在");
}
if(sysUser.getStatus() == 0){
throw new CustomizedException(201,"用户已停用");
}
//如果用户存在,则查询用户具有的权限
List<String> auth = sysMenuService.getUserPermissionList(sysUser.getId());
//将集合中的元素转换成SimpleGrantedAuthority,通过构造方法
// public SimpleGrantedAuthority(String role) {
// Assert.hasText(role, "A granted authority textual representation is required");
// this.role = role;
// }
ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
for (String s : auth) {
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(s));
}
return new CustomizedUser(sysUser, simpleGrantedAuthorities);
}
5.6.2 配置redis
- 在spring-security模块中配置redis。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 修改自定义的UsernamePasswordAuthenticationFilter方法:
//新增成员属性
//redisTemplate,用于操作redis
private RedisTemplate redisTemplate;
//修改成功方法
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain,Authentication authResult) {
CustomizedUser principal = (CustomizedUser) authResult.getPrincipal();
//验证成功则把权限放在redis中,便于后续查询
Collection<GrantedAuthority> authorities = principal.getAuthorities();
//将权限列表转换成JSON串
String jsonString = JSON.toJSONString(authorities);
//将JSON串存储在redis中
redisTemplate.opsForValue().set(principal.getUsername(),jsonString);
//创建token并返回
String token
= JWTHelper.createToken(principal.getSysUser().getId(), principal.getSysUser().getUsername());
HashMap<String, Object> stringObjectHashMap = new HashMap<>();
stringObjectHashMap.put("token",token);
//将map作为响应体返回
ResponseUtil.resultResponse(response, Result.ok(stringObjectHashMap));
}
- 自定义TokenAuthenticationFilter方法修改:
//在将token封装到对象中时,同时从redis中获取用户权限进行封装
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
String token = request.getHeader("token");
if(!StringUtils.isEmpty(token)){
//获取用户名
String username = JWTHelper.getUsername(token);
//从redis中查询用户权限
String s = (String) redisTemplate.opsForValue().get(username);
//将字符串转换为对象
List<SimpleGrantedAuthority> simpleGrantedAuthorities = JSON.parseArray(s, SimpleGrantedAuthority.class);
return new UsernamePasswordAuthenticationToken(username,null, simpleGrantedAuthorities);
}
return null;
}
- 在配置文件中配置redis
spring:
redis:
host: localhost
port: 6379
database: 0
timeout: 1800000
password:
jedis:
pool:
max-active: 20 #最大连接数
max-wait: -1 #最大阻塞等待时间(负数表示没限制)
max-idle: 5 #最大空闲
min-idle: 0 #最小空闲
要注意,如果redis在虚拟机或另一台linux机器上,那么要成功连接redis,必须关闭linux防火墙,然后将redis配置文件中的bind 127.0.0.1注释掉,将protected-mode改为no。否则会连接错误。
5.6.3 修改配置类
//开启注解功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
//注入redisTemplate
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http
//关闭csrf
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
//.antMatchers("/admin/system/index/login").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
.addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
.addFilter(new MyUsernamePasswordAuthenticationFilter(authenticationManager(),redisTemplate));
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
5.6.4 在方法上添加注解
@ApiOperation("删除菜单")
@DeleteMapping("/remove/{id}")
@PreAuthorize("hasAuthority('bnt.sysMenu.remove')")
public Result removeMenu(@PathVariable Long id){
return Result.ok();
}
5.6.5 自定义异常处理
//当没有权限访问时,执行下面的方法
@ExceptionHandler(AccessDeniedException.class)
@ResponseBody
public Result error(AccessDeniedException accessDeniedException){
return Result.build(null,ResultCodeEnum.NO_PERMISSION);
}