SpringSecurity

SpringSecurity

本Demo地址:https://gitee.com/bai-xiaoyun/spring-security-demo/tree/master

依赖包(spring-boot-starter-security)导入后,Spring Security就默认提供了许多功能将整个应用给保护了起来:

​ 1、要求经过身份验证的用户才能与应用程序进行交互

​ 2、创建好了默认登录表单

​ 3、生成用户名为user的随机密码并打印在控制台上

​ 4、CSRF攻击防护、Session Fixation攻击防护

​ 5、等等等等…

1、构建基本项目框架

依赖配置:暂时先不引入SpringSecurity

    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.10.RELEASE
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            io.jsonwebtoken
            jjwt
            0.9.0
        








        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.1
        


        
            mysql
            mysql-connector-java
        


        
            org.projectlombok
            lombok
        

        
            org.springframework.boot
            spring-boot-starter-data-redis
        



    

    
        8
        8
        UTF-8
    

用户实体类:

@TableName("sys_user")
@Data
public class SysUser {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String username;
    private String password;
    private String name;

    @TableLogic(value="0",delval = "1")
    private Integer deleted;
}

角色实体类:

@Data
@TableName("sys_role")
public class SysRole {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String roleName;

    @TableLogic(value = "0",delval = "1")
    private Integer deleted;
}

菜单实体类:

@Data
@TableName("sys_menu")
public class SysMenu {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Integer parent_id;
    private String name;
    private Integer type;
    @TableLogic(value = "0",delval = "1")
    private Integer deleted;

}

用户角色实体类:

@Data
@TableName("sys_user_role")
public class SysUserRole {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Integer userId;
    private Integer roleId;
    @TableLogic(value = "0",delval = "1")
    private Integer deleted;

}

菜单角色实体类

@Data
@TableName("sys_menu_role")
public class SysMenuRole {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Integer menuId;
    private Integer roleId;
    @TableLogic(value = "0",delval = "1")
    private Integer deleted;
}

yml配置文件:

server:
  port: 8080
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8&useSSL=false
    username: root
    password: root

用户的crud

@RequestMapping("/system/user")
@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/list/{page}/{limit}")
    private Result getUserList(@PathVariable(name = "page") Integer page,
                               @PathVariable(name = "limit") Integer limit){
        IPage iPage=new Page<>(page,limit);
        userService.page(iPage);

        return new Result<>(200,"查询成功",iPage);
    }


    @PostMapping("/add")
    private Result addUser(@RequestBody SysUser sysUser){
        userService.save(sysUser);
        return new Result(200,"添加成功",null );
    }

    @PutMapping("/update")
    private Result updateUser(@RequestBody SysUser sysUser){
        userService.updateById(sysUser);
        return new Result(200,"修改成功",null );
    }

    @DeleteMapping("/delete/{userId}")
    private Result deleteUser(@PathVariable Integer userId){
        userService.removeById(userId);
        return new Result(200,"删除成功",null );
    }


}

测试:

http://localhost:8080/system/user/list/1/5

)

访问成功

SpringSecurity_第1张图片

2、加入SpringSecurity依赖


    org.springframework.boot
    spring-boot-starter-security

再次访问http://localhost:8080/system/user/list/1/5,会直接跳转到SpringSecurity默认的登录页面(样式没显示出来)

SpringSecurity_第2张图片

用户名:user

密码:控制台有输出

SpringSecurity_第3张图片

3、用户认证

用户认证流程:

SpringSecurity_第4张图片

Spring Security中三个核心组件:

​ 1、Authentication:存储了认证信息,代表当前登录用户

​ 2、SeucirtyContext:上下文对象,用来获取Authentication

​ 3、SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

其中SecurityContextHolder原理非常简单,就是使用ThreadLocal来保证一个线程中传递同一个对象!

Authentication中是什么信息呢:

​ 1、Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象

​ 2、Credentials:用户凭证,一般是密码

​ 3、Authorities:用户权限

Spring Security是怎么进行用户认证的呢?

AuthenticationManager 就是Spring Security用于执行身份验证的组件,只需要调用它的authenticate方法即可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter这个过滤器中进行认证的,该过滤器负责认证逻辑。

AuthenticationManager的校验逻辑非常简单:

获得用户账号密码后先通过PasswordEncoder组件,进行密码加密后,交由**UserDetailsService** 处理,接口只有一个方法loadUserByUsername(String username),通过用户名查询用户对象,查询到对象之后封装到*UserDetails** ,该接口中提供了账号、密码等通用属性。然后通过PasswordEncoder组件对密码进行校验

①加密器PasswordEncoder

自定义MD5加密工具类:

public final class MD5 {

    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }


}

自定义加密处理组件:CustomMd5PasswordEncoder

@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
    
    //加密
    @Override
    public String encode(CharSequence charSequence) {
        return MD5.encrypt(charSequence.toString());
    }

    //是否匹配
    @Override
    public boolean matches(CharSequence charSequence, String s) {

        return s.equals(MD5.encrypt(charSequence.toString()));
    }
}

②用户对象UserDetails

该接口就是我们所说的用户对象,它提供了用户的一些通用属性,源码如下:

public interface UserDetails extends Serializable {
	/**
     * 用户权限集合(这个权限对象现在不管它,到权限时我会讲解)
     */
    Collection getAuthorities();
    /**
     * 用户密码
     */
    String getPassword();
    /**
     * 用户名
     */
    String getUsername();
    /**
     * 用户没过期返回true,反之则false
     */
    boolean isAccountNonExpired();
    /**
     * 用户没锁定返回true,反之则false
     */
    boolean isAccountNonLocked();
    /**
     * 用户凭据(通常为密码)没过期返回true,反之则false
     */
    boolean isCredentialsNonExpired();
    /**
     * 用户是启用状态返回true,反之则false
     */
    boolean isEnabled();
}

实际开发中我们的用户属性各种各样,这些默认属性可能是满足不了(比如没有id属性),所以我们一般会自己实现该接口,然后设置好我们实际的用户实体对象。实现此接口要重写很多方法比较麻烦,我们可以继承Spring Security提供的org.springframework.security.core.userdetails.User类,该类实现了UserDetails接口帮我们省去了重写方法的工作:

public class CustomerUser extends User {
    private SysUser sysUser;
    public CustomerUser(SysUser sysUser, Collection authorities) {
        super(sysUser.getUsername(), sysUser.getPassword(), authorities);
        this.sysUser=sysUser;
    }
    
    public SysUser getSysUser() {
        return sysUser;
    }

    public void setSysUser(SysUser sysUser) {
        this.sysUser = sysUser;
    }
}

③业务对象UserDetailsService

该接口很简单只有一个方法loadUserByUsername:

public interface UserDetailsService {
    /**
     * 根据用户名获取用户对象(获取不到直接抛异常)
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

我们实现该接口,就完成了自己的业务

@Component
public class UserDetailServiceImpl implements UserDetailsService {
   @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userMapper.selectOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username));

        if(null == sysUser) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        //返回用户对象,权限列表(暂时设置为空)
        return new CustomerUser(sysUser, Collections.emptyList());
    }
}

AuthenticationManager校验所调用的三个组件我们就已经做好实现了!

此时我们就可以通过默认的登录页面实现查询数据库的认证过程了!

4、添加登录过滤器

自定义自己的登录逻辑(采用token的方式)

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    public TokenLoginFilter(AuthenticationManager authenticationManager){
        this.setAuthenticationManager(authenticationManager);
        //指定登录接口和处理方式
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/login","POST"));

    }


    /**
     * 登录认证
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class);

            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());

            return this.getAuthenticationManager().authenticate(authenticationToken);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 登录成功->返回token
     * @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 {
        CustomerUser customUser = (CustomerUser) auth.getPrincipal();
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());

        Map 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 {
        ResponseUtil.out(response,Result.build(null,444,"登录失败"));
    }



}

配置Security

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
        http
                //关闭csrf
                .csrf().disable()
                // 开启跨域以便前端调用接口
                .cors().and()
                .authorizeRequests()
                // 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
                .antMatchers("/admin/login").permitAll()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()

                .addFilter(new TokenLoginFilter(authenticationManager()));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    /**
     * 配置哪些请求不拦截
     * 排除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");
    }
}

添加工具类:ResponseUtil

package com.atguigu.common.util;

import com.atguigu.common.result.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

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();
        }
    }
}

认证解析token

因为用户登录状态在token中存储在客户端,所以每次请求接口请求头携带token, 后台通过自定义token过滤器拦截解析token完成认证并填充用户信息实体

import com.iflytek.util.JwtHelper;
import com.iflytek.util.ResponseUtil;
import com.iflytek.util.Result;
import com.iflytek.util.ResultCodeEnum;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.Collections;

/**
 * 

* 认证解析token过滤器 *

*/ public class TokenAuthenticationFilter extends OncePerRequestFilter { public TokenAuthenticationFilter() { } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // logger.info("uri:"+request.getRequestURI()); //如果是登录接口,直接放行 if("/admin/login".equals(request.getRequestURI())) { chain.doFilter(request, response); return; } UsernamePasswordAuthenticationToken authentication = getAuthentication(request); if(null != authentication) { //放到上下文对象中 SecurityContextHolder.getContext().setAuthentication(authentication); //放行请求 chain.doFilter(request, response); } else { ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION)); } } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { // token置于header里 String token = request.getHeader("token"); logger.info("token:"+token); if (!StringUtils.isEmpty(token)) { String username = JwtHelper.getUsername(token); logger.info("useruame:"+username); if (!StringUtils.isEmpty(username)) { //返回一个认证对象 return new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList()); } } return null; } }

将解析token的过滤器也加入配置类WebSecurityConfig中

.and()
//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
.addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager()));

5、用户授权

修改UserDetailsServiceImpl中的loadUserByUsername方法,设置用户权限

之前我们只封装了用户信息,没有设置用户权限

@Component
public class UserDetailServiceImpl implements UserDetailsService {
   @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userMapper.selectOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username));

        if(null == sysUser) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        //查询用户权限
        List permission = userPermission(sysUser);


        //返回用户对象,权限列表(暂时设置为空)
        return new CustomerUser(sysUser, permission);
    }
    public  List userPermission(SysUser sysUser){
      List menus=userMapper.findRouter(sysUser.getId());

        List collect = menus.stream().filter(item->item.getPerms()!=null).map(SysMenu::getPerms).collect(Collectors.toList());
        List authorities = new ArrayList<>();

        for (String perm : collect) {
            authorities.add(new SimpleGrantedAuthority(perm.trim()));
        }

        return authorities;

    }
}

配置redis

将登录成功的用户权限存放到redis中,使用token认证时,直接从redis中取出该用户的权限


    org.springframework.boot
    spring-boot-starter-data-redis

配置文件

spring:
  redis:
    host: 192.168.6.100
    port: 6379
    database: 0
    timeout: 1800000
    password: root
    jedis:
      pool:
        max-active: 20 #最大连接数
        max-wait: -1    #最大阻塞等待时间(负数表示没限制)
        max-idle: 5    #最大空闲
        min-idle: 0     #最小空闲

第一次登录成功我们将权限数据保存到reids

注意RedisTemplate的注入方式是通过构造器注入

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private RedisTemplate redisTemplate;
    public TokenLoginFilter(AuthenticationManager authenticationManager,RedisTemplate redisTemplate){
        this.setAuthenticationManager(authenticationManager);
        //指定登录接口和处理方式
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/login","POST"));
        this.redisTemplate=redisTemplate;
    }


    /**
     * 登录认证
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginVo loginVo = new ObjectMapper().readValue(request.getInputStream(), LoginVo.class);

            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());

            return this.getAuthenticationManager().authenticate(authenticationToken);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 登录成功->返回token
     * @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 {
        CustomerUser customUser = (CustomerUser) auth.getPrincipal();
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());

        Map map = new HashMap<>();
        map.put("token", token);

        //存入redis
        redisTemplate.opsForValue().set(customUser.getUsername(),customUser.getAuthorities());



        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 {
        ResponseUtil.out(response,Result.build(null,444,"登录失败"));
    }



}

修改TokenAuthenticationFilter认证成功后,从redis里面获取权限数据

RedisTemplate注入方式同上

/**
 * 

* 认证解析token过滤器 *

*/ 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/login".equals(request.getRequestURI())) { chain.doFilter(request, response); return; } UsernamePasswordAuthenticationToken authentication = getAuthentication(request); if(null != authentication) { //放到上下文对象中 SecurityContextHolder.getContext().setAuthentication(authentication); //放行请求 chain.doFilter(request, response); } else { ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION)); } } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { // token置于header里 String token = request.getHeader("token"); logger.info("token:"+token); if (!StringUtils.isEmpty(token)) { String username = JwtHelper.getUsername(token); logger.info("useruame:"+username); if (!StringUtils.isEmpty(username)) { //返回一个认证对象 //从redis中读取用户权限 Collection authorities = (Collection)redisTemplate.opsForValue().get(username); return new UsernamePasswordAuthenticationToken(username, null, authorities); } } return null; } }

在WebSecurityConfig中注入

@Autowired
private RedisTemplate redisTemplate;

将redisTemplate传参给两个过滤器

            .and()
            //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
            .addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
            .addFilter(new TokenLoginFilter(authenticationManager(),redisTemplate));

并开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认

在配置类上启用

@EnableGlobalMethodSecurity(prePostEnabled = true)

控制controller层接口权限

Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限

通过@PreAuthorize标签控制controller层接口权限

@PreAuthorize("hasAuthority('btn.sysUser.list')")
@GetMapping("/list/{page}/{limit}")
private Result getUserList(@PathVariable(name = "page") Integer page,
                           @PathVariable(name = "limit") Integer limit){
    IPage iPage=new Page<>(page,limit);
    userService.page(iPage);

    return  Result.ok(iPage);
}

6、捕获权限异常

捕获没有权限异常AccessDeniedException

在全局异常处理器中添加以下方法

/**
 * spring security异常
 * @param e
 * @return
 */
@ExceptionHandler(AccessDeniedException.class)
@ResponseBody
public Result error(AccessDeniedException e) throws AccessDeniedException {
    return Result.build(null, ResultCodeEnum.PERMISSION);
}

注意:AccessDeniedException导入的是Spring Security中的,别导错包

你可能感兴趣的:(SpringSecurity,microsoft)