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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM