zoukankan      html  css  js  c++  java
  • Spring Security整合JWT,实现单点登录,So Easy~!

    前面整理过一篇 SpringBoot Security前后端分离,登录退出等返回json数据,也就是用Spring Security,基于SpringBoot2.1.4 RELEASE前后端分离的情况下,实现了登陆登出的功能,亮点就在于以JSON的形式接收返回参数。这个是针对单个后台服务的, 登录信息都存储在SecurityContextHolder缓存里。如果是两个或两个以上的应用呢,那该怎么办?Session是不能用了,Cookie自然也不能用,毕竟它俩是一对的。

    曾想过用OAuth2来解决这个问题,但是OAuth2太复杂,首先理解概念就需要花费一些时间,而且里面的授权服务器、资源服务器、客户端等等让人傻傻分不清,还有四种授权模式,要反复衡量,到底要用哪一种,概念还没有扯清楚就开始纠结使用哪一个了。从概念入手不是个好主意,也不是个轻松的主意。在理解OAuth2的过程中,想到自己的项目是前后端分离的,离不开JSON,无意中遇见JWT。JWT是什么玩意,咦,难道是自己苦苦寻求的吗?!

    那么,什么是JWT呢?看看专家介绍 阮一峰的网络日志,才知道,JWT 是JSON Web Token的简称,它解决的就是跨域问题。看来,要找的就是它,简单的,但也是管用的。

    继续深究,JWT到底是怎样和SpringSecurity结合的呢。下面上代码,在上代码前先说明一下,在本次实例中,涉及到两个项目,一个项目是登录的项目A,另一个项目是根据token进行访问的项目B。其中B项目没有登录,也不会涉及登录,只要有Token就可以访问,Token失效了就访问不了了。

    A项目是登录的项目,也是一个只能通过登录进行访问的后台服务。B项目就是一个服务,只要用户在A项目登录了,就可以访问。


     
    设计图-1

    A项目配置,代码如下

    第一步,A项目 POM.xml 引入文件
     <!-- spring-security 和 jwt 引入 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <dependency>            
                <groupId>io.jsonwebtoken</groupId>            
                <artifactId>jjwt</artifactId>            
                <version>0.9.0</version>        
            </dependency>
    
    第二步,A项目SecurityConfig配置
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    import com.example.demo.filter.JWTAuthenticationFilter;
    import com.example.demo.filter.JWTLoginFilter;
    
    /**
     * SpringSecurity的配置
     * 参考网址:https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226
     * @author 程就人生
     * @date 2019年5月26日
     */
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Autowired
        private UserDetailsService myCustomUserService;
    
        @Autowired
        private MyPasswordEncoder myPasswordEncoder;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                //关闭跨站请求防护
                .cors().and().csrf().disable()
                //允许不登陆就可以访问的方法,多个用逗号分隔
                .authorizeRequests().antMatchers("/test").permitAll()
                //其他的需要授权后访问
                .anyRequest().authenticated()
                
                .and()         
                //增加登录拦截
                .addFilter(new JWTLoginFilter(authenticationManager()))     
                //增加是否登陸过滤
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                // 前后端分离是无状态的,所以暫時不用session,將登陆信息保存在token中。
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            
    
        }
    
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            //覆盖UserDetailsService类
            auth.userDetailsService(myCustomUserService)
            //覆盖默认的密码验证类
            .passwordEncoder(myPasswordEncoder);
        }
    }
    
    第三步,实现配置文件中自定义的类
    1. MyPasswordEncoder类实现了默认的PasswordEncoder 接口,可以对密码加密和密码对比进行个性化定制
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Component;
    
     /**
     * 自定义的密码加密方法,实现了PasswordEncoder接口
     * @author 程就人生
     * @date 2019年5月26日
     */
    @Component
    public class MyPasswordEncoder implements PasswordEncoder {
    
        @Override
        public String encode(CharSequence charSequence) {
            //加密方法可以根据自己的需要修改
            return charSequence.toString();
        }
    
        @Override
        public boolean matches(CharSequence charSequence, String s) {
            return encode(charSequence).equals(s);
        }
    }
    
    1. MyCustomUserService 实现了框架默认的UserDetailsService,可以根据username从数据库获取用户,查看用户是否存在
    /**
     * 登录专用类,用户登陆时,通过这里查询数据库
     * 自定义类,实现了UserDetailsService接口,用户登录时调用的第一类
     * @author 程就人生
     * @date 2019年5月26日
     */
    @Component
    public class MyCustomUserService implements UserDetailsService {
    
        /**
         * 登陆验证时,通过username获取用户的所有权限信息
         * 并返回UserDetails放到spring的全局缓存SecurityContextHolder中,以供授权器使用
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //在这里可以自己调用数据库,对username进行查询,看看在数据库中是否存在
            MyUserDetails myUserDetail = new MyUserDetails();
            myUserDetail.setUsername(username);
            myUserDetail.setPassword("123456");
            return myUserDetail;
        }
    }
    
    1. MyUserDetails 实现了框架的UserDetails接口,可以在该类中根据需要添加自己必需的属性
    import java.util.Collection;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    /**
     * 实现了UserDetails接口,只留必需的属性,也可添加自己需要的属性
     * @author 程就人生
     * @date 2019年5月26日
     */
    public class MyUserDetails implements UserDetails {
    
        private static final long serialVersionUID = 1L;
    
        //登录用户名
        private String username;
        //登录密码
        private String password;
    
        private Collection<? extends GrantedAuthority> authorities;
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
            this.authorities = authorities;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return this.authorities;
        }
    
        @Override
        public String getPassword() {
            return this.password;
        }
    
        @Override
        public String getUsername() {
            return this.username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    1. JWTLoginFilter 实现了框架自带的UsernamePasswordAuthenticationFilter 接口,对拦截做处理,以便登录成功后,在头部设置token返回;不管登录成功还是失败,都有JSON数据返回
    import java.io.PrintWriter;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    import javax.servlet.FilterChain;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    import com.example.demo.entity.User;
    import com.example.demo.security.MyUserDetails;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm; 
    
    /**
     * 验证用户名密码正确后,生成一个token,放在header里,返回给客户端 
     * @author 程就人生
     * @date 2019年5月26日
     */
    public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter { 
        
        private AuthenticationManager authenticationManager;     
        
        public JWTLoginFilter(AuthenticationManager authenticationManager) { 
            
            this.authenticationManager = authenticationManager;    
            
        } 
        
        /**
         * 接收并解析用户凭证,出現错误时,返回json数据前端
         */
        @Override    
        public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res){        
            try {            
                User user =new ObjectMapper().readValue(req.getInputStream(), User.class);             
                return authenticationManager.authenticate(                    
                        new UsernamePasswordAuthenticationToken(                            
                                user.getUsername(),                            
                                user.getPassword(),                            
                                new ArrayList<>())            
                        );        
                } catch (Exception e) {
                    try {
                        //未登錄出現賬號或密碼錯誤時,使用json進行提示
                        res.setContentType("application/json;charset=utf-8");
                        res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                        PrintWriter out = res.getWriter();                  
                        Map<String,Object> map = new HashMap<String,Object>();
                        map.put("code",HttpServletResponse.SC_UNAUTHORIZED);
                        map.put("message","账号或密码错误!");
                        out.write(new ObjectMapper().writeValueAsString(map));
                        out.flush();
                        out.close();
                    } catch (Exception e1) {
                        e1.printStackTrace();
                    }
                    throw new RuntimeException(e);        
                }    
        }
        
        /**
         * 用户登录成功后,生成token,并且返回json数据给前端
         */
        @Override    
        protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,FilterChain chain, Authentication auth){         
            
            //json web token构建
            String token = Jwts.builder()    
                    //此处为自定义的、实现org.springframework.security.core.userdetails.UserDetails的类,需要和配置中设置的保持一致
                    //此处的subject可以用一个用户名,也可以是多个信息的组合,根据需要来定
                    .setSubject(((MyUserDetails) auth.getPrincipal()).getUsername())    
                    //设置token过期时间,24小時
                    .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000)) 
                    
                    //设置token签名、密钥
                    .signWith(SignatureAlgorithm.HS512, "MyJwtSecret")       
                    
                    .compact();  
            
            //返回token
            res.addHeader("Authorization", "Bearer " + token); 
            
            try {
                //登录成功時,返回json格式进行提示
                res.setContentType("application/json;charset=utf-8");
                res.setStatus(HttpServletResponse.SC_OK);
                PrintWriter out = res.getWriter();                  
                Map<String,Object> map = new HashMap<String,Object>();
                map.put("code",HttpServletResponse.SC_OK);
                map.put("message","登陆成功!");
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
    }
    
    1. JWTAuthenticationFilter 类实现了BasicAuthenticationFilter 接口,对Controller中需要登录后才能访问的方法进行了拦截,没有登录,则不能访问,返回JSON信息进行提示
    import java.io.PrintWriter;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.Map;
    
    import javax.servlet.FilterChain;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import io.jsonwebtoken.Jwts;
    
    /**
     * 是否登陆验证方法
     * @author 程就人生
     * @date 2019年5月26日
     */
    public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
        
        public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
            super(authenticationManager);
        }
    
        /**
         * 對請求進行過濾
         */
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {        
            try {
                //请求体的头中是否包含Authorization
                String header = request.getHeader("Authorization");     
                //Authorization中是否包含Bearer,有一个不包含时直接返回
                if (header == null || !header.startsWith("Bearer ")) {
                    chain.doFilter(request, response);
                    responseJson(response);
                    return;        
                } 
                //获取权限失败,会抛出异常
                UsernamePasswordAuthenticationToken authentication = getAuthentication(request); 
                //获取后,将Authentication写入SecurityContextHolder中供后序使用
                SecurityContextHolder.getContext().setAuthentication(authentication); 
                chain.doFilter(request, response);
            } catch (Exception e) {
                responseJson(response);
                e.printStackTrace();
            }     
        }
    
        /**
         * 未登錄時的提示
         * @param response
         */
        private void responseJson(HttpServletResponse response){
            try {
                //未登錄時,使用json進行提示
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                PrintWriter out = response.getWriter();                 
                Map<String,Object> map = new HashMap<String,Object>();
                map.put("code",HttpServletResponse.SC_FORBIDDEN);
                map.put("message","请登录!");
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
        
        /**
         * 通过token,获取用户信息
         * @param request
         * @return
         */
        private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {        
            String token = request.getHeader("Authorization");        
            if (token != null) {            
                //通过token解析出用户信息            
                String user = Jwts.parser()   
                        //签名、密钥
                        .setSigningKey("MyJwtSecret")                    
                        .parseClaimsJws(token.replace("Bearer ", ""))                    
                        .getBody()                    
                        .getSubject();     
                //不为null,返回
                if (user != null) {                
                    return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());            
                }           
                return null;        
            }        
            return null;    
        } 
            
    }
    
    1. 在登录过滤器中接收参数的实体类,也可以直接接收,这一个类不是必须的
    public class User {
        
        private long id;
        private String username;
        private String password;
    
        public long getId() {
            return id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    }
    

    B项目的配置

    第一步,在pom中引入必须的架包
    <!-- spring-security 和 jwt 引入 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <dependency>            
                <groupId>io.jsonwebtoken</groupId>            
                <artifactId>jjwt</artifactId>            
                <version>0.9.0</version>        
            </dependency>
    
    第二步,增加SecurityConfig配置文件
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    
    import com.example.demo.filter.JWTAuthenticationFilter;
    
    /**
     * SpringSecurity的配置
     * 参考网址:https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226
     * @author 程就人生
     * @date 2019年5月26日
     */
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                //关闭跨站请求防护
                .cors().and().csrf().disable()
                //允许不登陆就可以访问的方法,多个用逗号分隔
                .authorizeRequests()
                //其他的需要授权后访问
                .anyRequest().authenticated()
                
                .and()
                //增加是否登陸过滤
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                // 前后端分离是无状态的,所以暫時不用session,將登陆信息保存在token中。
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
        }
        
    }
    
    第三步,在增加对方法是否登录进行拦截的过滤器
    import java.io.PrintWriter;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.Map;
    
    import javax.servlet.FilterChain;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import io.jsonwebtoken.Jwts;
    
    /**
     * 是否登陆验证方法
     * @author 程就人生
     * @date 2019年5月26日
     */
    public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
        
        public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
            super(authenticationManager);
        }
    
        /**
         * 對請求進行過濾
         */
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {        
            try {
                //请求体的头中是否包含Authorization
                String header = request.getHeader("Authorization");     
                //Authorization中是否包含Bearer,有一个不包含时直接返回
                if (header == null || !header.startsWith("Bearer ")) {
                    chain.doFilter(request, response);
                    responseJson(response);
                    return;        
                } 
                //获取权限失败,会抛出异常
                UsernamePasswordAuthenticationToken authentication = getAuthentication(request); 
                //获取后,将Authentication写入SecurityContextHolder中供后序使用
                SecurityContextHolder.getContext().setAuthentication(authentication); 
                chain.doFilter(request, response);
            } catch (Exception e) {
                responseJson(response);
                e.printStackTrace();
            }     
        }
    
        /**
         * 未登錄時的提示
         * @param response
         */
        private void responseJson(HttpServletResponse response){
            try {
                //未登錄時,使用json進行提示
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                PrintWriter out = response.getWriter();                 
                Map<String,Object> map = new HashMap<String,Object>();
                map.put("code",HttpServletResponse.SC_FORBIDDEN);
                map.put("message","请登录!");
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
        
        /**
         * 通过token,获取用户信息
         * @param request
         * @return
         */
        private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {        
            String token = request.getHeader("Authorization");        
            if (token != null) {            
                //通过token解析出用户信息            
                String user = Jwts.parser()   
                        //签名盐
                        .setSigningKey("MyJwtSecret")                    
                        .parseClaimsJws(token.replace("Bearer ", ""))                    
                        .getBody()                    
                        .getSubject();     
                //不为null,返回
                if (user != null) {                
                    return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());            
                }           
                return null;        
            }        
            return null;    
        } 
            
    }
    

    从B项目的配置中,可以看出,B项目配置的太简洁了,只需要拦截一下没有登录的请求,连登录也都省了。

    A和B项目中分别添加一个Controller,用于测试

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 测试用例
     * @author 程就人生
     * @date 2019年5月26日
     */
    @RestController
    public class IndexController {
    
        @GetMapping("/index")
        public Object index(){
            
            return "index";
        }
    }
    

    使用测试工具进行测试

    第一步,测试A项目和B项目的index是否能访问,结果都不能访问,测试结果OK
     
    测试结果-1

     
    测试结果-2
    第二步,通过登录获取token,登录成功后,返回了JSON格式的提示,返回的token在头部,点击响应头,获取token
     
    测试结果-3

     
    测试结果-4
    第三步,将token拷贝至A项目index的头部,B项目index的头部,测试结果ok,都可以访问,也可以把token时间设置的短一些,测试一下token过期了,是否还能访问。
     
    测试结果-5

     
    测试结果-6

    最后,感觉一下Token的结构,去掉前面固定的Bearer ,后面的分成三个部分,中间用点隔开,这个就简单了解下吧。

    • Header(头部)
    • Payload(负载)
    • Signature(签名)
    Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJmZW5nIiwiZXhwIjoxNTU4OTUxMjM5fQ.X7lOhHJljxnVcNEckYSX22rgTDN0ToRJLaPb_1dAoPzx6q_eN5B5iOxO2GXoNUllIfQG6SrdJhgYzKZPTMsDIg
    

    Spring Security整合JWT,实现单点登录的功能,到此就告一段落了,看起来是不是很简单呢,那就动手试一试吧。



    作者:程就人生
    链接:https://www.jianshu.com/p/8bd4a6e27e7f
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 相关阅读:
    数据终端设备与无线通信模块之间串行通信链路复用协议(TS27.010)在嵌入式系统上的开发【转】
    设备树网址【原创笔记】
    clock()、time()、clock_gettime()和gettimeofday()函数的用法和区别【转】
    ajaxFileUpload SyntaxError: syntax error
    工厂模式
    程序猿都是project师吗?
    [android开发之内容更新类APP]二、这几日的结果
    Java实现将指定目录内的指定类型的文件归类
    移动支付之智能IC卡与Android手机进行NFC通信
    Java并发框架——AQS堵塞队列管理(一)——自旋锁
  • 原文地址:https://www.cnblogs.com/telwanggs/p/11162110.html
Copyright © 2011-2022 走看看