zoukankan      html  css  js  c++  java
  • Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十五):Spring Security 版本

    在线演示

    演示地址:http://139.196.87.48:9002/kitty

    用户名:admin 密码:admin

    技术背景

    到目前为止,我们使用的权限认证框架是 Shiro,虽然 Shiro 也足够好用并且简单,但对于 Spring 官方主推的安全框架 Spring Security,用户群也是甚大的,所以我们这里把当前的代码切分出一个 shiro-cloud 分支,作为 Shiro + Spring Cloud 技术的分支代码,dev 和 master 分支将替换为 Spring Security + Spring Cloud 的技术栈,并在后续计划中集成 Spring Security OAuth2 实现单点登录功能。

    代码实现

    Maven依赖

    移除shiro依赖,添加Spring Scurity和JWT依赖包,jwt目前的最新版本是0.9.1。

    <!-- spring security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- jwt -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>${jwt.version}</version>
    </dependency>

    权限注解

    替换Shiro的权限注解为Spring Security的权限注解。

    格式如下:

    @PreAuthorize("hasAuthority('sys:menu:view')")

    SysMenuController.java

    package com.louis.kitty.admin.controller;
    
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.louis.kitty.admin.model.SysMenu;
    import com.louis.kitty.admin.sevice.SysMenuService;
    import com.louis.kitty.core.http.HttpResult;
    
    /**
     * 菜单控制器
     * @author Louis
     * @date Oct 29, 2018
     */
    @RestController
    @RequestMapping("menu")
    public class SysMenuController {
    
        @Autowired
        private SysMenuService sysMenuService;
        
        @PreAuthorize("hasAuthority('sys:menu:add') AND hasAuthority('sys:menu:edit')")
        @PostMapping(value="/save")
        public HttpResult save(@RequestBody SysMenu record) {
            return HttpResult.ok(sysMenuService.save(record));
        }
    
        @PreAuthorize("hasAuthority('sys:menu:delete')")
        @PostMapping(value="/delete")
        public HttpResult delete(@RequestBody List<SysMenu> records) {
            return HttpResult.ok(sysMenuService.delete(records));
        }
    
        @PreAuthorize("hasAuthority('sys:menu:view')")
        @GetMapping(value="/findNavTree")
        public HttpResult findNavTree(@RequestParam String userName) {
            return HttpResult.ok(sysMenuService.findTree(userName, 1));
        }
        
        @PreAuthorize("hasAuthority('sys:menu:view')")
        @GetMapping(value="/findMenuTree")
        public HttpResult findMenuTree() {
            return HttpResult.ok(sysMenuService.findTree(null, 0));
        }
    }

    Spring Security注解默认是关闭的,可以通过在配置类添加以下注解开启。

    @EnableGlobalMethodSecurity(prePostEnabled = true)

    安全配置

    添加安全配置类, 继承 WebSecurityConfigurerAdapter,配置URL验证策略和相关过滤器以及自定义的登录验证组件。

    WebSecurityConfig.java

    package com.louis.kitty.admin.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
    
    import com.louis.kitty.admin.security.JwtAuthenticationFilter;
    import com.louis.kitty.admin.security.JwtAuthenticationProvider;
    
    /**
     * Spring Security Config
     * @author Louis
     * @date Nov 20, 2018
     */
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsService;
        
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 使用自定义身份验证组件
            auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
            http.cors().and().csrf().disable()
                .authorizeRequests()
                // 跨域预检请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // web jars
                .antMatchers("/webjars/**").permitAll()
                // 查看SQL监控(druid)
                .antMatchers("/druid/**").permitAll()
                // 首页和登录页面
                .antMatchers("/").permitAll()
                .antMatchers("/login").permitAll()
                // swagger
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources").permitAll()
                .antMatchers("/v2/api-docs").permitAll()
                .antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
                // 验证码
                .antMatchers("/captcha.jpg**").permitAll()
                // 服务监控
                .antMatchers("/actuator/**").permitAll()
                // 其他所有请求需要身份认证
                .anyRequest().authenticated();
            // 退出登录处理器
            http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
            // 登录认证过滤器
            http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
        
    }

    登录验证组件

    继承 DaoAuthenticationProvider, 实现自定义的登录验证组件,覆写密码验证逻辑。

    JwtAuthenticationProvider.java

    package com.louis.kitty.admin.security;
    
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    import com.louis.kitty.admin.util.PasswordEncoder;
    
    /**
     * 身份验证提供者
     * @author Louis
     * @date Nov 20, 2018
     */
    public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
    
        public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
            setUserDetailsService(userDetailsService);
        }
    
        @Override
        protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            if (authentication.getCredentials() == null) {
                logger.debug("Authentication failed: no credentials provided");
                throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
    
            String presentedPassword = authentication.getCredentials().toString();
            String salt = ((JwtUserDetails) userDetails).getSalt();
            // 覆写密码验证逻辑
            if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
                logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    
    }

    用户认证信息查询组件

    实现 UserDetailsService 接口,定义用户认证信息查询组件,用于获取认证所需的用户信息和授权信息。

    UserDetailsServiceImpl.java

    package com.louis.kitty.admin.security;
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import com.louis.kitty.admin.model.SysUser;
    import com.louis.kitty.admin.sevice.SysUserService;
    
    /**
     * 用户登录认证信息查询
     * @author Louis
     * @date Nov 20, 2018
     */
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private SysUserService sysUserService;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            SysUser user = sysUserService.findByName(username);
            if (user == null) {
                throw new UsernameNotFoundException("该用户不存在");
            }
            // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
            Set<String> permissions = sysUserService.findPermissions(user.getName());
            List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
            return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);
        }
    }

    用户认证信息封装

    上面 UserDetailsService 查询的信息需要封装到实现 UserDetails 接口的封装对象里。

    JwtUserDetails.java

    package com.louis.kitty.admin.security;
    import java.util.Collection;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import com.fasterxml.jackson.annotation.JsonIgnore;
    
    /**
     * 安全用户模型
     * @author Louis
     * @date Nov 20, 2018
     */
    public class JwtUserDetails implements UserDetails {
    
        private static final long serialVersionUID = 1L;
        
        private String username;
        private String password;
        private String salt;
        private Collection<? extends GrantedAuthority> authorities;
    
        JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
            this.username = username;
            this.password = password;
            this.salt = salt;
            this.authorities = authorities;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @JsonIgnore
        @Override
        public String getPassword() {
            return password;
        }
    
        public String getSalt() {
            return salt;
        }
        
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
    
        @JsonIgnore
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @JsonIgnore
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @JsonIgnore
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @JsonIgnore
        @Override
        public boolean isEnabled() {
            return true;
        }
    
    }

    登录接口

    因为我们没有使用内置的 formLogin 登录处理过滤器,所以需要调用登录认证流程,修改登录接口,加入系统登录认证调用。

    SysLoginController.java

       /**
         * 登录接口
         */
        @PostMapping(value = "/login")
        public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
            String username = loginBean.getAccount();
            String password = loginBean.getPassword();
            String captcha = loginBean.getCaptcha();...
         // 系统登录认证 JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager); return HttpResult.ok(token); }

    Spring Security 的登录认证过程是通过调用 AuthenticationManager 的 authenticate(token) 方法实现的。

    登录流程中主要是返回一个认证好的 Authentication 对象,然后保存到上下文供后续进行授权的时候使用。

    登录认证成功之后,会利用JWT生成 token 返回给客户端,后续的访问都需要携带此 token 来进行认证。

    SecurityUtils.java

        /**
         * 系统登录认证
         * @param request
         * @param username
         * @param password
         * @param authenticationManager
         * @return
         */
        public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
            JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
            token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            // 执行登录认证过程
            Authentication authentication = authenticationManager.authenticate(token);
            // 认证成功存储认证信息到上下文
            SecurityContextHolder.getContext().setAuthentication(authentication);
            // 生成令牌并返回给客户端
            token.setToken(JwtTokenUtils.generateToken(authentication));
            return token;
        }

    令牌生成器

    令牌生成器主要是利用JWT生成所需的令牌,部分代码如下。

    JwtTokenUtils.java

    /**
     * JWT工具类
     * @author Louis
     * @date Nov 20, 2018
     */
    public class JwtTokenUtils implements Serializable {
    
        /**
         * 生成令牌
         * @param userDetails 用户
         * @return 令牌
         */
        public static String generateToken(Authentication authentication) {
            Map<String, Object> claims = new HashMap<>(3);
            claims.put(USERNAME, SecurityUtils.getUsername(authentication));
            claims.put(CREATED, new Date());
            claims.put(AUTHORITIES, authentication.getAuthorities());
            return generateToken(claims);
        }
    
        /**
         * 从数据声明生成令牌
         * @param claims 数据声明
         * @return 令牌
         */
        private static String generateToken(Map<String, Object> claims) {
            Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
        }
    }

    登录认证过滤器

    登录认证过滤器继承 BasicAuthenticationFilter,在访问任何URL的时候会被此过滤器拦截,通过调用 SecurityUtils.checkAuthentication(request) 检查登录状态。

    JwtAuthenticationFilter.java

    package com.louis.kitty.admin.security;
    
    import java.io.IOException;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
    
    import com.louis.kitty.admin.util.SecurityUtils;
    
    /**
     * 登录认证过滤器
     * @author Louis
     * @date Nov 20, 2018
     */
    public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    
        
        @Autowired
        public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
            super(authenticationManager);
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            // 获取token, 并检查登录状态
            SecurityUtils.checkAuthentication(request);
            chain.doFilter(request, response);
        }
        
    }

    登录认证检查

    登录验证检查是通过 SecurityUtils.checkAuthentication(request) 来完成的。

    SecurityUtils.java

        /**
         * 获取令牌进行认证
         * @param request
         */
        public static void checkAuthentication(HttpServletRequest request) {
            // 获取令牌并根据令牌获取登录认证信息
            Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
            // 设置登录认证信息到上下文
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

    上面的登录验证是通过 JwtTokenUtils.getAuthenticationeFromToken(request),来验证令牌并返回登录信息的。

    JwtTokenUtils.java

        /**
         * 根据请求令牌获取登录认证信息
         * @param token 令牌
         * @return 用户名
         */
        public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
            Authentication authentication = null;
            // 获取请求携带的令牌
            String token = JwtTokenUtils.getToken(request);
            if(token != null) {
                // 请求令牌不能为空
                if(SecurityUtils.getAuthentication() == null) {
                    // 上下文中Authentication为空
                    Claims claims = getClaimsFromToken(token);
                    if(claims == null) {
                        return null;
                    }
                    String username = claims.getSubject();
                    if(username == null) {
                        return null;
                    }
                    if(isTokenExpired(token)) {
                        return null;
                    }
                    Object authors = claims.get(AUTHORITIES);
                    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
                    if (authors != null && authors instanceof List) {
                        for (Object object : (List) authors) {
                            authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
                        }
                    }
                    authentication = new JwtAuthenticatioToken(username, null, authorities, token);
                } else {
                    if(validateToken(token, SecurityUtils.getUsername())) {
                        // 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
                        authentication = SecurityUtils.getAuthentication();
                    }
                }
            }
            return authentication;
        }

    清除Shiro配置

    清除掉 config 包下的 ShiroConfig 配置类。

    清除 oautho2 包下有关 Shiro 的相关代码。

    清除掉 sys_token 表和相关操作代码。

    源码下载

    后端:https://gitee.com/liuge1988/kitty

    前端:https://gitee.com/liuge1988/kitty-ui.git


    作者:朝雨忆轻尘
    出处:https://www.cnblogs.com/xifengxiaoma/ 
    版权所有,欢迎转载,转载请注明原文作者及出处。

  • 相关阅读:
    DQL2.3.开始使用Dgraph基本类型和操作
    DQL2.7.开始使用Dgraph模糊搜索
    启动时查看配置文件application.yml
    从源码角度,带你研究什么是三级缓存
    Spring Boot 在启动时进行配置文件加解密
    论Redis分布式锁的正确使用姿势
    SpringBoot中整合Redis、Ehcache使用配置切换 并且整合到Shiro中
    在项目中,如何保证幂等性
    给你的SpringBoot做埋点监控JVM应用度量框架Micrometer
    从源码层面带你实现一个自动注入注解
  • 原文地址:https://www.cnblogs.com/xifengxiaoma/p/9987278.html
Copyright © 2011-2022 走看看