权限-说说SpringSecurity

权限-说说SpringSecurity

想想以前为了赶课设的自己,当时用的Shiro,现在用SpringSecurity去探索了一番。以Layui作为前端为例子:前端框架:layuimini,我已经默认你清楚RCBA模型了。用户角色资源,加两个中间表也就是五张表,还有一些特殊情况,我补充了一张表,用户资源关系表。所以我数据库底层用了6张表维护我的权限模块。

image-20211230165319903

1、以前让我烦呐init.json

layuimini在index页面,是读取init.json获取菜单信息的,这是死数据,而我们要让不同用户有不同菜单,就必须自己构造出这样的init.json.

    @GetMapping("/getMenuInfo")
    @ApiOperation("获取用户菜单栏")
    public Map<String,Object> getMenuInfo(Principal principal){
        log.info("===系统正在获取用户菜单栏中====");
        Map<String,Object> resultMap = new LinkedHashMap<>();
        Map<String,Object> homeInfo = new LinkedHashMap<>();
        Map<String,Object> logoInfo = new LinkedHashMap<>();
        homeInfo.put("title","首页");
        homeInfo.put("href","/welcome");
        logoInfo.put("title","Repository");
        logoInfo.put("image","static/images/logo.png");
        logoInfo.put("href","");
        resultMap.put("homeInfo",homeInfo);
        resultMap.put("logoInfo",logoInfo);
        UmsAdmin admin = adminService.getAdminByUsername(principal.getName());
        List<MenuNode> menuNodeList =  adminService.getMenuInfo(admin.getId());
        resultMap.put("menuInfo",menuNodeList);
        log.info("===系统成功获取用户菜单栏=====");
        return resultMap;
    }

2、SpringSecurity的用户详情

在SpringSecurity中都是基于用户名的认证,他提供了UserDetail接口,让我们可以扩展自己的UserDetail.同时也开发或UserDetailService业务处理类,里头的方法就是根据用户名获取用户详情。所以只要把获取用户详情和自己的数据库产生联系就可以了。

2.1、我的用户详情

package com.wnx.mall.security;


import cn.hutool.core.util.StrUtil;
import com.wnx.mall.mbg.model.UmsAdmin;
import com.wnx.mall.mbg.model.UmsPermission;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * SpringSecurity需要的用户详情
 */
public class AdminUserDetails implements UserDetails {
    /**
     * 系统用户
     */
    private UmsAdmin umsAdmin;
    /**
     * 用户权限
     */
    private List<UmsPermission> permissionList;
	
    public AdminUserDetails(UmsAdmin umsAdmin, List<UmsPermission> permissionList) {
        this.umsAdmin = umsAdmin;
        this.permissionList = permissionList;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //返回当前用户的权限
        return permissionList.stream()
                .filter(permission -> StrUtil.isNotBlank(permission.getValue()))
                .map(permission ->new SimpleGrantedAuthority(permission.getValue()))
                .collect(Collectors.toList());
    }


    @Override
    public String getPassword() {
        return umsAdmin.getPassword();
    }

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

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

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

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

    @Override
    public boolean isEnabled() {
        return umsAdmin.getStatus().equals(1);
    }
}

2.2、我的处理用户详情的业务类

 @Bean
    @Override
    public UserDetailsService userDetailsService() {
        //获取登录用户信息
        return username -> {
            UmsAdmin admin = adminService.getAdminByUsername(username); //联系~!!~~
            if (admin != null) {
                List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId()); //联系~!!~~
                String permissionStr = permissionList.stream()
                        .filter(p -> p.getValue() != null)
                        .map(UmsPermission::getValue)
                        .collect(Collectors.joining(","));

                log.info(username+"用户正在从数据库成功读取到权限{}",permissionStr);

                return new AdminUserDetails(admin,permissionList); //看上面构造方法
            }
            throw new UsernameNotFoundException("用户名或密码错误");
        };
    }

2.3、JWT

用户登录成功后,我返回了JWT,并保存到客户端的cookies中!因为请求的无状态性,为了鉴别请求是谁发起,我的JWT过滤器,就处理的这个问题。有这个JWT的cookies,就放行,没有非法请求,打回去,拒绝访问。

package com.wnx.mall.security;


import com.wnx.mall.common.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * JWT登录授权过滤器
 *
 * @author macro
 * @date 2018/4/26
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String authHeader = null;

        //从cookies中取出jwt
        Cookie[] cookies = request.getCookies();
        if (cookies!=null && cookies.length>0){
            for (Cookie cookie : cookies) {
                if ("token".equals(cookie.getName())){
                    authHeader = cookie.getValue();
                }
            }
        }


        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("系统正在检测该用户===》:{}", username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    LOGGER.info("认证通过,当前认证用户===》:{}", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                }
            }
        }


        chain.doFilter(request, response);
    }
}

3、用户登录调用的接口

@ApiOperation("登录以后返回token")
    @PostMapping(value = "/login")
    public CommonResult login(@RequestBody @Validated UmsAdminLoginParam umsAdminLoginParam) {
        log.info("=========系统正在进行用户登录=======");
        String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword());
        if (token == null) { return CommonResult.validateFailed("用户名或密码错误"); }
        Map<String, String> tokenMap = new HashMap<>(2);
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        log.info("=========系统登录成功,返回token{}=======",tokenMap);
        return CommonResult.success(tokenMap);
    }
    /**
     *  登录功能
     * @param username 用户名
     * @param password 密码
     * @return
     */
    @Override
    public String login(String username, String password) {
        String token = null;
        try {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (!passwordEncoder.matches(password, userDetails.getPassword())) { throw new BadCredentialsException("密码不正确"); }
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            token = jwtTokenUtil.generateToken(userDetails);
        } catch (AuthenticationException e) {
            log.warn("登录异常:{}", e.getMessage());
        }
        return token;
    }

4、怎么拦截?

拦截就要在配置类中配置了。配置哪些请求是放行的,哪些请求需要认证。意思: 除了上面放行的请求,其他都必须认证。

   @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/static/**",
                        "/swagger-resources/**",
                        "/v2/api-docs/**"
                )
                .permitAll()
                .antMatchers("/admin/login", "/admin/register")
                .permitAll()
                .antMatchers(HttpMethod.OPTIONS)
                .permitAll()
                .anyRequest()
                .authenticated();



        httpSecurity.formLogin().loginPage("/toLoginPage");
        httpSecurity.logout().logoutSuccessUrl("/");

        //开启对frame框架的支持
        httpSecurity.headers().frameOptions().disable();

        // 禁用缓存
        httpSecurity.headers().cacheControl();
        // 添加JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定义未授权和未登录结果返回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

5、源码

可见仓库管理系统

权限-说说SpringSecurity

想想以前为了赶课设的自己,当时用的Shiro,现在用SpringSecurity去探索了一番。以Layui作为前端为例子:前端框架:layuimini,我已经默认你清楚RCBA模型了。用户角色资源,加两个中间表也就是五张表,还有一些特殊情况,我补充了一张表,用户资源关系表。所以我数据库底层用了6张表维护我的权限模块。

image-20211230165319903

1、以前让我烦呐init.json

layuimini在index页面,是读取init.json获取菜单信息的,这是死数据,而我们要让不同用户有不同菜单,就必须自己构造出这样的init.json.

    @GetMapping("/getMenuInfo")
    @ApiOperation("获取用户菜单栏")
    public Map<String,Object> getMenuInfo(Principal principal){
        log.info("===系统正在获取用户菜单栏中====");
        Map<String,Object> resultMap = new LinkedHashMap<>();
        Map<String,Object> homeInfo = new LinkedHashMap<>();
        Map<String,Object> logoInfo = new LinkedHashMap<>();
        homeInfo.put("title","首页");
        homeInfo.put("href","/welcome");
        logoInfo.put("title","Repository");
        logoInfo.put("image","static/images/logo.png");
        logoInfo.put("href","");
        resultMap.put("homeInfo",homeInfo);
        resultMap.put("logoInfo",logoInfo);
        UmsAdmin admin = adminService.getAdminByUsername(principal.getName());
        List<MenuNode> menuNodeList =  adminService.getMenuInfo(admin.getId());
        resultMap.put("menuInfo",menuNodeList);
        log.info("===系统成功获取用户菜单栏=====");
        return resultMap;
    }

2、SpringSecurity的用户详情

在SpringSecurity中都是基于用户名的认证,他提供了UserDetail接口,让我们可以扩展自己的UserDetail.同时也开发或UserDetailService业务处理类,里头的方法就是根据用户名获取用户详情。所以只要把获取用户详情和自己的数据库产生联系就可以了。

2.1、我的用户详情

package com.wnx.mall.security;


import cn.hutool.core.util.StrUtil;
import com.wnx.mall.mbg.model.UmsAdmin;
import com.wnx.mall.mbg.model.UmsPermission;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * SpringSecurity需要的用户详情
 */
public class AdminUserDetails implements UserDetails {
    /**
     * 系统用户
     */
    private UmsAdmin umsAdmin;
    /**
     * 用户权限
     */
    private List<UmsPermission> permissionList;
	
    public AdminUserDetails(UmsAdmin umsAdmin, List<UmsPermission> permissionList) {
        this.umsAdmin = umsAdmin;
        this.permissionList = permissionList;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //返回当前用户的权限
        return permissionList.stream()
                .filter(permission -> StrUtil.isNotBlank(permission.getValue()))
                .map(permission ->new SimpleGrantedAuthority(permission.getValue()))
                .collect(Collectors.toList());
    }


    @Override
    public String getPassword() {
        return umsAdmin.getPassword();
    }

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

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

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

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

    @Override
    public boolean isEnabled() {
        return umsAdmin.getStatus().equals(1);
    }
}

2.2、我的处理用户详情的业务类

 @Bean
    @Override
    public UserDetailsService userDetailsService() {
        //获取登录用户信息
        return username -> {
            UmsAdmin admin = adminService.getAdminByUsername(username); //联系~!!~~
            if (admin != null) {
                List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId()); //联系~!!~~
                String permissionStr = permissionList.stream()
                        .filter(p -> p.getValue() != null)
                        .map(UmsPermission::getValue)
                        .collect(Collectors.joining(","));

                log.info(username+"用户正在从数据库成功读取到权限{}",permissionStr);

                return new AdminUserDetails(admin,permissionList); //看上面构造方法
            }
            throw new UsernameNotFoundException("用户名或密码错误");
        };
    }

2.3、JWT

用户登录成功后,我返回了JWT,并保存到客户端的cookies中!因为请求的无状态性,为了鉴别请求是谁发起,我的JWT过滤器,就处理的这个问题。有这个JWT的cookies,就放行,没有非法请求,打回去,拒绝访问。

package com.wnx.mall.security;


import com.wnx.mall.common.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * JWT登录授权过滤器
 *
 * @author macro
 * @date 2018/4/26
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String authHeader = null;

        //从cookies中取出jwt
        Cookie[] cookies = request.getCookies();
        if (cookies!=null && cookies.length>0){
            for (Cookie cookie : cookies) {
                if ("token".equals(cookie.getName())){
                    authHeader = cookie.getValue();
                }
            }
        }


        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("系统正在检测该用户===》:{}", username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    LOGGER.info("认证通过,当前认证用户===》:{}", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                }
            }
        }


        chain.doFilter(request, response);
    }
}

3、用户登录调用的接口

@ApiOperation("登录以后返回token")
    @PostMapping(value = "/login")
    public CommonResult login(@RequestBody @Validated UmsAdminLoginParam umsAdminLoginParam) {
        log.info("=========系统正在进行用户登录=======");
        String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword());
        if (token == null) { return CommonResult.validateFailed("用户名或密码错误"); }
        Map<String, String> tokenMap = new HashMap<>(2);
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        log.info("=========系统登录成功,返回token{}=======",tokenMap);
        return CommonResult.success(tokenMap);
    }
    /**
     *  登录功能
     * @param username 用户名
     * @param password 密码
     * @return
     */
    @Override
    public String login(String username, String password) {
        String token = null;
        try {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (!passwordEncoder.matches(password, userDetails.getPassword())) { throw new BadCredentialsException("密码不正确"); }
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            token = jwtTokenUtil.generateToken(userDetails);
        } catch (AuthenticationException e) {
            log.warn("登录异常:{}", e.getMessage());
        }
        return token;
    }

4、怎么拦截?

拦截就要在配置类中配置了。配置哪些请求是放行的,哪些请求需要认证。意思: 除了上面放行的请求,其他都必须认证。

   @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/static/**",
                        "/swagger-resources/**",
                        "/v2/api-docs/**"
                )
                .permitAll()
                .antMatchers("/admin/login", "/admin/register")
                .permitAll()
                .antMatchers(HttpMethod.OPTIONS)
                .permitAll()
                .anyRequest()
                .authenticated();



        httpSecurity.formLogin().loginPage("/toLoginPage");
        httpSecurity.logout().logoutSuccessUrl("/");

        //开启对frame框架的支持
        httpSecurity.headers().frameOptions().disable();

        // 禁用缓存
        httpSecurity.headers().cacheControl();
        // 添加JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定义未授权和未登录结果返回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

5、源码

可见仓库管理系统


版权声明:本文为WNX10086原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。