权限管理系统-0.4.2

5.5 Spring Security

5.5.1 引入spring security

  1. 在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>
  1. 在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 {
}
  1. 在service-oa模块中引入spring-security模块:
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>spring-security</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
  1. 启动项目测试,直接访问后端接口就会进入认证页面,默认用户名是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

  1. 在spring-security模块中配置redis。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 修改自定义的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));
}
  1. 自定义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;
    }
  1. 在配置文件中配置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);
    }
  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值