zoukankan      html  css  js  c++  java
  • spring boot:spring security整合jwt实现登录和权限验证(spring boot 2.3.3)

    一,为什么使用jwt?

    1,什么是jwt?

    Json Web Token,

    它是JSON风格的轻量级的授权和身份认证规范,

    可以实现无状态、分布式的Web应用授权

    2,jwt的官网:

    https://jwt.io/

    java实现的jwt的开源项目:

    https://github.com/jwtk/jjwt

    3,使用jwt的好处?

    客户端请求不依赖服务端的信息,多次向服务端请求不需要必须访问到同一台物理服务器上
    服务端的集群和状态对客户端透明
    服务端可以任意的迁移和伸缩,方便进行集群化部署
    减小服务端存储压力

    说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest

             对应的源码可以访问这里获取: https://github.com/liuhongdi/

    说明:作者:刘宏缔 邮箱: 371125307@qq.com

    二,演示项目的相关信息

    1,项目地址:

    https://github.com/liuhongdi/securityjwt

    2,项目功能说明:

            演示了使用jwt保存用户token,

            适用于接口站的用户信息保存

    3,项目结构;如图:

    三,配置文件说明

    1,pom.xml

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <!--security begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <!--jjwt begin-->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.1</version>
            </dependency>
    
            <!--thymeleaf begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
            <!--fastjson begin-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.73</version>
            </dependency>
    
          <!--jaxb-->
            <dependency>
                <groupId>javax.xml.bind</groupId>
                <artifactId>jaxb-api</artifactId>
                <version>2.3.0</version>
            </dependency>
            <dependency>
                <groupId>com.sun.xml.bind</groupId>
                <artifactId>jaxb-impl</artifactId>
                <version>2.3.0</version>
            </dependency>
            <dependency>
                <groupId>com.sun.xml.bind</groupId>
                <artifactId>jaxb-core</artifactId>
                <version>2.3.0</version>
            </dependency>
            <dependency>
                <groupId>javax.activation</groupId>
                <artifactId>activation</artifactId>
                <version>1.1.1</version>
            </dependency>
    
            <!--mysql mybatis begin-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.3</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>

    2,application.properties

    #error
    server.error.include-stacktrace=always
    #error
    logging.level.org.springframework.web=trace
    
    #thymeleaf
    spring.thymeleaf.cache=false
    spring.thymeleaf.encoding=UTF-8
    spring.thymeleaf.mode=HTML
    spring.thymeleaf.prefix=classpath:/templates/
    spring.thymeleaf.suffix=.html
    
    #mysql
    spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=lhddemo
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    #mybatis
    mybatis.mapper-locations=classpath:/mapper/*Mapper.xml
    mybatis.type-aliases-package=com.example.demo.mapper

    3,数据表:

     建表sql:

    CREATE TABLE `sys_user` (
     `userId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
     `userName` varchar(100) NOT NULL DEFAULT '' COMMENT '用户名',
     `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码',
     `nickName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵称',
     PRIMARY KEY (`userId`),
     UNIQUE KEY `userName` (`userName`)
    ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'
    INSERT INTO `sys_user` (`userId`, `userName`, `password`, `nickName`) VALUES
    (1, 'lhd', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '老刘'),
    (2, 'admin', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '管理员'),
    (3, 'merchant', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '商户老张');

    说明:3个密码都是111111,仅供演示使用

    CREATE TABLE `sys_user_role` (
     `urId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
     `userId` int(11) NOT NULL DEFAULT '0' COMMENT '用户id',
     `roleName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色id',
     PRIMARY KEY (`urId`),
     UNIQUE KEY `userId` (`userId`,`roleName`)
    ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表'
    INSERT INTO `sys_user_role` (`urId`, `userId`, `roleName`) VALUES
    (1, 2, 'ADMIN'),
    (2, 3, 'MERCHANT');

    四,  java代码说明

     1,WebSecurityConfig.java

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Resource
        private UserAuthenticationEntryPoint userAuthenticationEntryPoint;
    
        @Autowired
        private UserDetailsService jwtUserDetailsService;
    
        @Autowired
        private JwtRequestFilter jwtRequestFilter;
    
        @Resource
        private UserAccessDeniedHandler userAccessDeniedHandler;
    
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
            // 本示例不需要使用CSRF
            httpSecurity.csrf().disable();
            httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            httpSecurity.authorizeRequests().antMatchers("/home/**").permitAll();
            // 认证页面不需要权限
            httpSecurity.authorizeRequests().
                    antMatchers("/auth/authenticate").permitAll().
                    antMatchers("/admin/**").hasAnyRole("ADMIN").
                    //其他页面
                    anyRequest().authenticated();
            //登录页面 模拟客户端
            httpSecurity.formLogin().loginPage("/home/login").permitAll();
            //access deny
            httpSecurity.exceptionHandling().accessDeniedHandler(userAccessDeniedHandler);
            //unauthorized
            httpSecurity.exceptionHandling().authenticationEntryPoint(userAuthenticationEntryPoint);
            //验证请求是否正确
            httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }

    2,UserAuthenticationEntryPoint.java

    @Component
    public class UserAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
            // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
            System.out.println("i am 401");
            ServletUtil.printRestResult(RestResult.error(ResponseCode.WEB_401));
        }
    }

    说明:匿名用户访问无权限资源时的异常

    3,UserAccessDeniedHandler.java

    @Component("UserAccessDeniedHandler")
    public class UserAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
            //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
            System.out.println("UserAccessDeniedHandler");
            ServletUtil.printRestResult(RestResult.error(ResponseCode.WEB_403));
        }
    }

    说明:非匿名用户访问无权限访问的资源时的异常

    4,SecUser.java

    public class SecUser extends User {
        //用户id
        private int userid;
        //昵称
        private String nickname;
    
        public SecUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
            super(username, password, authorities);
        }
    
        public SecUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
            super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        }
    
        public String getNickname() {
            return nickname;
        }
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
    
        public int getUserid() {
            return userid;
        }
        public void setUserid(int userid) {
            this.userid = userid;
        }
    }

    扩展spring security user类

    5,JwtAuthticationFilter.java

    @Component
    public class JwtAuthticationFilter implements Filter {
    
        @Resource
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private JwtTokenUtil jwtTokenUtil;
    
        @Autowired
        private JwtUserDetailsService userDetailsService;
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            System.out.println("----------------AuthticationFilter init");
        }
        //过滤功能
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            //得到当前的url
            HttpServletRequest request = (HttpServletRequest)servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            String path = request.getServletPath();
            if (path.equals("/auth/authenticate")) {
                 System.out.println("auth path:"+path);
                 //得到请求的post参数
                String username = "";
                String password = "";
                try {
                    BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream()));
                    StringBuffer sb=new StringBuffer();
                    String s=null;
                    while((s=br.readLine())!=null){
                        sb.append(s);
                    }
                    JSONObject jsonObject = JSONObject.parseObject(sb.toString());
                    username = jsonObject.getString("username");
                    password = jsonObject.getString("password");
                    //System.out.println("name:"+name+" age:"+age);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("username:"+username);
                System.out.println("password:"+password);
                String authResult = "";
                try{
                    authResult = authenticate(username,password);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("authResult:"+authResult);
    //验证通过后生成token返回
    if ("success".equals(authResult)) { final UserDetails userDetails = userDetailsService.loadUserByUsername(username); final String token = jwtTokenUtil.generateToken(userDetails); Map<String, String> mapData = new HashMap<String, String>(); mapData.put("token", token); ServletUtil.printRestResult(RestResult.success(mapData)); } else if ("badcredential".equals(authResult)){ ServletUtil.printRestResult(RestResult.error(ResponseCode.LOGIN_FAIL)); } else { ServletUtil.printRestResult(RestResult.error(ResponseCode.ERROR)); } return; } else { System.out.println("not auth path:"+path); filterChain.doFilter(servletRequest, servletResponse); } } @Override public void destroy() { System.out.println("----------------filter destroy"); } private String authenticate(String username, String password) throws Exception { try { System.out.println("username:"+username); System.out.println("password:"+password); authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); System.out.println("authenticate:will return success"); return "success"; } catch (DisabledException e) { throw new Exception("USER_DISABLED", e); } catch (BadCredentialsException e) { System.out.println("BadCredentialsException"); System.out.println(e.toString()); //throw new Exception("INVALID_CREDENTIALS", e); return "badcredential"; } } }

    用来实现登录的filter,验证通过后生成token返回

    6,JwtRequestFilter.java

    @Component
    public class JwtRequestFilter extends OncePerRequestFilter {
    
        @Autowired
        private JwtUserDetailsService jwtUserDetailsService;
    
        @Autowired
        private JwtTokenUtil jwtTokenUtil;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
            final String requestTokenHeader = request.getHeader("Authorization");
            String username = null;
            String jwtToken = null;
            // JWT Token 获取请求头部的 Bearer
            System.out.println("filter:header:"+requestTokenHeader);
            //判断,从token中得到username
            if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
                //System.out.println("filter :requestTokenHeader not null and start with bearer");
                jwtToken = requestTokenHeader.substring(7);
                try {
                    username = jwtTokenUtil.getUsernameFromToken(jwtToken);
                } catch (IllegalArgumentException e) {
                    System.out.println("Unable to get JWT Token");
                } catch (ExpiredJwtException e) {
                    System.out.println("JWT Token has expired");
                } catch (MalformedJwtException e) {
                    System.out.println("JWT Token MalformedJwtException");
                }
            } else {
                //System.out.println("filter :requestTokenHeader is null || not start with bearer");
                //logger.warn("JWT Token does not begin with Bearer String");
            }
    
            // 验证,username,如果验证合法则保存到SecurityContextHolder
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                //System.out.println("filter:username!=null");
                UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
                // JWT 验证通过 使用Spring Security 管理
                if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    //System.out.println("usernamePasswordAuthenticationToken:"+usernamePasswordAuthenticationToken.toString());
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                } else {
                   // System.out.println("jwtTokenUtil.validateToken not success");
                }
            } 
            chain.doFilter(request, response);
        }
    }

    处理每次的请求,如果有token,则从token获取用户信息,验证用户信息合法,则把从数据库中得到的用户的相关信息保存到SecurityContextHolder

    7,JwtUserDetailsService.java

    @Service
    public class JwtUserDetailsService implements UserDetailsService {
        @Resource
        private SysUserService sysUserService;
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            System.out.println("-----loadUserByUsername");
            SysUser oneUser = sysUserService.getOneUserByUsername(username);//数据库查询 看用户是否存在
            String encodedPassword = oneUser.getPassword();
            Collection<GrantedAuthority> collection = new ArrayList<>();//权限集合
            //用户角色role前面要添加ROLE_
            List<String> roles = oneUser.getRoles();
            System.out.println(roles);
            for (String roleone : roles) {
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+roleone);
                collection.add(grantedAuthority);
            }
            //给用户增加用户id和昵称
            SecUser user = new SecUser(username,encodedPassword,collection);
            user.setUserid(oneUser.getUserId());
            user.setNickname(oneUser.getNickName());
            return user;
        }
    }

    从数据库得到用户信息

    8,JwtTokenUtil.java

    @Component
    public class JwtTokenUtil implements Serializable {
        private static final long serialVersionUID = -2550185165626007488L;
        public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
    
        private String secret = "liuhongdi";
        //retrieve username from jwt token
        public String getUsernameFromToken(String token) {
            return getClaimFromToken(token, Claims::getSubject);
        }
        //retrieve expiration date from jwt token
        public Date getExpirationDateFromToken(String token) {
            return getClaimFromToken(token, Claims::getExpiration);
        }
    
        public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
            final Claims claims = getAllClaimsFromToken(token);
            return claimsResolver.apply(claims);
        }
        //for retrieveing any information from token we will need the secret key
        private Claims getAllClaimsFromToken(String token) {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        }
        //check if the token has expired
        private Boolean isTokenExpired(String token) {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        }
        //generate token for user
        public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            return doGenerateToken(claims, userDetails.getUsername());
        }
        //generate token
        private String doGenerateToken(Map<String, Object> claims, String subject) {
            return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                    .signWith(SignatureAlgorithm.HS512, secret).compact();
        }
        //validate token
        public Boolean validateToken(String token, UserDetails userDetails) {
            final String username = getUsernameFromToken(token);
            return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
        }
    }

    处理JwtToken的工具类,用来生成token,验证token是否合法

    9,login.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>使用 jwt 登录页面</title>
    </head>
    <body>
    <div>
        <input type="text" id="userName" name="userName" value="" placeholder="username">
    </div>
    <div>
        <input type="password" id="password" name="password" value="" placeholder="password">
    </div>
    <div>
        <input type="button" id="btnSave" onclick="go_login()"  value="登录">
    </div>
    <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.js"></script>
    <script>
            //登录
            function go_login() {
                var username=$("#userName").val();
                var password=$("#password").val();
                if ($("#userName").val() == "") {
                    alert('userName is empty');
                    $("#userName").focus();
                    return false;
                }
                if ($("#password").val() == "") {
                    alert('password is empty');
                    $("#password").focus();
                    return false;
                }
                var postData = {
                    "username":username ,
                    "password" : password
                }
                $.ajax({
                    cache: true,
                    type: "POST",
                    url: "/auth/authenticate",
                    contentType: "application/json;charset=UTF-8",
                    data:JSON.stringify(postData),
                    dataType: "json",
                    async: false,
                    error: function (request) {
                        console.log("Connection error");
                    },
                    success: function (data) {
                        //save token
                        console.log("data:");
                        console.log(data);
                        if (data.code == 0) {
                            //success
                            alert("success:"+data.msg+";token:"+data.data.token);
                            //save token
                            localStorage.setItem("token",data.data.token);
                        } else {
                            //failed
                            alert("failed:"+data.msg);
                        }
                    }
                });
            };
    </script>
    </body>
    </html>

    10,其他代码可从github上查看

    五,测试效果

     1,登录,访问:

    http://127.0.0.1:8080/home/login

    用admin登录:

    可以看到返回的token

    2,查看session信息:访问:

    http://127.0.0.1:8080/home/getsession

    点击:get session info

     点击:get admin info:

     可以正常访问

    3,用merchant登录:

    点击 get admin info:

    提示拒绝访问

    六,查看spring boot的版本:

      .   ____          _            __ _ _
     /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
     \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v2.3.3.RELEASE)
  • 相关阅读:
    漫谈施工企业信息化规划(修订)
    SOAOffice 中间件,北京科翰软件
    组建“建筑施工企业信息化技术交流”QQ群(102226121),欢迎参与!
    国产、免费业务流程梳理工具SAM,炎黄盈动公司产品
    (参考)OpenExpressApp架构-信息系统开发平台
    逍遥笔输入法,哈!
    昨天开了一天的会!!
    昨天回到北京了!
    幸福是什么?
    周末辽宁兴城
  • 原文地址:https://www.cnblogs.com/architectforest/p/13625729.html
Copyright © 2011-2022 走看看