SpringSecurity学习记录

一、spring-boot引入SpringSecurity

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

二、因为springboot的自动装配特性

因为springboot有自动装配的特性,所以,会自动实现导入包里的内容,在访问地址加端口号时会自动产生一个页面进行登录,
默认账号为:user
默认密码为控制台输出的内容

三、通过数据库自定义登录账号

显然默认配置是无法满足我们日常的使用,所以我们需要自定义,通过分析得到UserDetailsService是在登录时候获取用户的类,所以我们只需要将其自定义以下就可以。

1.在service.impl包下创建一个UserDetailsServiceImpl类继承UserDetailsService接口并实现其中的loadUserByUsername方法就可以用来调用自己数据库的账号

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserDAO userDAO; // user的mapper,用来调用数据库sql

    @Autowired
    private MenuDAO menuDAO; // menu的mapper,用来调用数据库sql

    @Override
    public UserDetails loadUserByUsername(String useName) throws UsernameNotFoundException {

        // 查询用户信息
        User user = userDAO.selectByUserName(useName);

        // 没有查询到用户抛出异常
        if (Objects.isNull(user)){
            throw new RuntimeException("当前不存在该用户名,请注册!");
        }

        // 查询对应的权限信息
        List<String> perms = menuDAO.selectPermsByUserId(user.getId());

        // 把数据封装成UserDetails返回
        return new LoginUser(user, perms);
    }
}

2.在上一步的的实现方法中发现我们需要返回一个UserDetails对象,该对象内包含我们的用户信息,所以只需要创建一个登录对象LoginUser实现UserDetails接口

public class LoginUser implements UserDetails {

    public LoginUser() {
    }

    public LoginUser(User user, List<String> perms) {
        this.user = user;
        this.perms = perms;
    }

    private User user; // 存入用户信息
    private List<String> perms; // 存入权限信息
    private List<GrantedAuthority> authorities; // 存入SpringSecurity所需要类型的权限信息,需要通过下面的流转换

    /**
     * 获取权限信息
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null){
            return authorities;
        }

        authorities = perms.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }
    
    /**
     * 这里必须使用自己的用户类中的getPassword()方法,不然会出现 Empty encoded password 错误
     * @return
     */
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public List<String> getPerms() {
        return perms;
    }

    public void setPerms(List<String> perms) {
        this.perms = perms;
    }

    public void setAuthorities(List<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }
}

到这里就实现了从数据库里获取用户信息。

3.到这里运行后发现用自己的用户名和密码是不能够登录上去的,因为SpringSecurity里自动加入了加密密码的功能.

3.1如果想要使用明文存储密码,则需要在明文上存储前加入{noop},如{noop}123456,123456就是我们的密码了,

3.2 如果想使用SpringSecurity的加密密码方法,可以在config包下创建一个SpringConfig类进行配置SpringSecurity

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 加密方式
     * 创建BCryptPasswordEncoder 注入容器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

3.3 到此,我们就实现了将自己的用户名和密码登录,不过,这个密码必须先进行加密后存入数据库才行,使用以下语句获取到加密后的密码

    @Autowired
    PasswordEncoder passwordEncoder;

    @Test
    public void passwordEncoder() {
        // 加密
        String encode = passwordEncoder.encode("123456");
        System.out.println(encode);
    }    

4.使用的自己的登录页面

4.1 先使用Controller写一个登录登出接口

@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    /**
     * 登录
     * @return
     */
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user) {

        return loginService.login(user);
    }

    /**
     * 退出登录
     * @return
     */
    @RequestMapping("/user/logout")
    public ResponseResult logout() {
        return loginService.logout();
    }
}

4.2 实现该接口

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {

        // 1. UsernamePasswordAuthenticationToken创建 Authentication对象
        // 传入 AuthenticationManager authenticate 用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // 认证没通过,给出提示
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("登陆失败");
        }
        // 认证通过,使用userid生成一个jwt jwt存入 ResponseResult 返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        // map=token:jwt
        Map<String, String> jwtMap = new HashMap<>();
        jwtMap.put("token", jwt);
        // 把完整的用户信息存入redis userid作为key
        redisCache.setCacheObject("token:" + userId, jwt); // token存入redis中
        redisCache.setCacheObject("login:" + userId, loginUser); // 登录的对象存入redis中
        return new ResponseResult(200, "登陆成功", jwtMap);
    }

    @Override
    public ResponseResult logout() {
        // 获取 SecurityContextHolder 中的用户id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userId = loginUser.getUser().getId();
        // 删除 redis 中的值
        redisCache.deleteObject("token:" + userId);
        redisCache.deleteObject("login:" + userId);
        return new ResponseResult(200, "注销成功");
    }
}

4.3 到此,我们就实现了登录登出自己的登录接口

4.4 当我们已经处于登录状态时,请求发送过来会在请求头中带有token,我们将该token解析得到userId,到redis数据库内查找该token,如果token一致则说明是已经登录的用户,则会通过,并将获取权限信息封装到Authentication中,告诉后面的调度,该请求是已经登录的,若请求头没有带有token或token有误,则会直接跳过。代码如下:JwtAuthenticationTokenFilter

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 需要前端在请求头中加入token 获取 token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)) { // token为空放行
            filterChain.doFilter(httpServletRequest, httpServletResponse);// 对请求放行
            return;
        }
        // 解析 token
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            filterChain.doFilter(httpServletRequest, httpServletResponse);// 对请求放行
            return;
        }
        // 从redis中获取token
        String redisToken = redisCache.getCacheObject("token:" + userId);
        if (!token.equals(redisToken)){
            filterChain.doFilter(httpServletRequest, httpServletResponse);// 对请求放行
            return;
        }

        // 从redis中获取用户信息
        String redisKey = "login:" + userId;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)){
            filterChain.doFilter(httpServletRequest, httpServletResponse);// 对请求放行
            return;
        }
        // 能访问到这里才是携带token访问成功
        // 存入SecurityContextHolder
        // 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        // 放行
        filterChain.doFilter(httpServletRequest, httpServletResponse);

    }
}

4.5 当我们实现JwtAuthenticationTokenFilter 的时候,发现并没有用,需要将其注入到SecurityConfig中

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 加密方式
     * 创建BCryptPasswordEncoder 注入容器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 重写
     * @return
     * @throws Exception
     */
    @Bean // 重写该方法来暴露 AuthenticationManager
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http
//                //关闭csrf
//                .csrf().disable()
//                //不通过Session获取SecurityContext
//                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//                .and()
//                .authorizeRequests()
//                // 对于登录接口 允许匿名访问
//                .antMatchers("/user/login").anonymous()
//                // 除上面外的所有请求全部需要鉴权认证
//                .anyRequest().authenticated();
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // permitAll 登不登陆都放行
                .antMatchers("/hello").permitAll()
                // 匿名可以访问
                .antMatchers("/user/login").anonymous()
                .anyRequest().authenticated();

        // 添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加过滤器

    }
}

5.配置异常处理

当我们认证和授权时候出现异常的时候,想要返回一个json格式的字符串给前端,这时候就需要下面的配置

5.1 AuthenticationEntryPointImpl 和 AccessDeniedHandlerImpl

5.1.1 AuthenticationEntryPointImpl为了 当授权时出现异常,捕获并返回异常json
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    // 当授权时出现异常,捕获并返回异常json
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        // 处理异常
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(httpServletResponse, json);

    }
}
5.1.2 AccessDeniedHandlerImpl 当认证时段出现异常,则会捕获异常,返回异常json
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    // 当认证时段出现异常,则会捕获异常,返回异常json
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        // 处理异常
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登录");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(httpServletResponse, json);

    }
}
5.1.3 将他们注入到 SecurityConfig中
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    /**
     * 加密方式
     * 创建BCryptPasswordEncoder 注入容器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 重写
     * @return
     * @throws Exception
     */
    @Bean // 重写该方法来暴露 AuthenticationManager
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http
//                //关闭csrf
//                .csrf().disable()
//                //不通过Session获取SecurityContext
//                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//                .and()
//                .authorizeRequests()
//                // 对于登录接口 允许匿名访问
//                .antMatchers("/user/login").anonymous()
//                // 除上面外的所有请求全部需要鉴权认证
//                .anyRequest().authenticated();
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // permitAll 登不登陆都放行
                .antMatchers("/hello").permitAll()
                // 匿名可以访问
                .antMatchers("/user/login").anonymous()
                .anyRequest().authenticated();

        // 添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加过滤器

        // 配置异常处理器
        http.exceptionHandling()
                // 认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                // 授权失败处理器
                .accessDeniedHandler(accessDeniedHandler);

        // 允许跨域
        http.cors();
    }
}

6.跨域问题

6.1 得先实现springboot的跨域,如下

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                // .allowedOriginPatterns("*") // 版本不对2.1.4没有
                // .allowedOrigins("*") // 自己加的,应该不能替代上面那个
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}

6.2 开启SecurityConfig内的跨域配置

        // 允许跨域
        http.cors();

7.自定义权限注解

7.1自定义

@Component("ex")
public class ExpressionRoot {

    public boolean hasAuthority(String authority){
        // 获取用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> perms = loginUser.getPerms();

        // 判断用户权限集合中是否存在authority
        return perms.contains(authority);
    }
}

7.2 使用自定义的权限注解

    @PreAuthorize("@ex.hasAuthority('system:dept:list')") // 自定义的
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值