1. 程式人生 > 實用技巧 >SpringSecurity 整合JWT實現無狀態登陸

SpringSecurity 整合JWT實現無狀態登陸

SpringSecurity 整合JWT實現無狀態登陸

案例使用SpringBoot作為基礎框架快速整合JWT

1.新增啟動依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.blogsx</groupId>
    <artifactId>springboot_security_jwt</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>

    <dependencies>
        <!-- web功能起步依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--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>0.7.0</version>
        </dependency>
        <!--  mybatis依賴-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- druid資料庫連線池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.9</version>
        </dependency>
        <!-- mysql驅動-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>

2.新增配置檔案並配置基本資訊

server.port=8080

# 資料庫連線相關配置
spring.datasource.url=jdbc:mysql:///springsecurity?characterEncoding=utf8&useSSL=true
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

# MyBatis註解形式掃描實體類路徑
mybatis.type-aliases-package=cn.blogsx.entity

# MyBatis XML形式配置檔案路徑
mybatis.config-locations=classpath:mybatis/mybatis-config.xml
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml

# 配置Jwt金鑰
jwt.secret=Alex

3.建立資料庫表資訊

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` int(11) NOT NULL,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
  `nameZh` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_dba', '資料庫管理員');
INSERT INTO `role` VALUES (2, 'ROLE_admin', '系統管理員');
INSERT INTO `role` VALUES (3, 'ROLE_user', '使用者');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL,
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
  `enabled` tinyint(1) NULL DEFAULT NULL,
  `locked` tinyint(1) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '$2a$10$8XXMNg8WQ8YlSIGGcgnaw./zrf2k6klkqXs0ezawj43VN7uh/m8Wu', 1, 0);
INSERT INTO `user` VALUES (2, 'admin', '$2a$10$8XXMNg8WQ8YlSIGGcgnaw./zrf2k6klkqXs0ezawj43VN7uh/m8Wu', 1, 0);
INSERT INTO `user` VALUES (3, 'alex', '$2a$10$8XXMNg8WQ8YlSIGGcgnaw./zrf2k6klkqXs0ezawj43VN7uh/m8Wu', 1, 0);

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `id` int(11) NOT NULL,
  `uid` int(11) NULL DEFAULT NULL,
  `rid` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Fixed;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 3, 3);

SET FOREIGN_KEY_CHECKS = 1;

4.新建測試介面

介面

@RestController
public class UserController {

    @RequestMapping("/hello")
    public Object hello() {
        String str  = "hello";
        return str;
    }
    //介面呼叫前判斷是否又admin角色
    @PreAuthorize("hasRole('admin')") //此處使用註解實現方法級的安全,也可以在SecurityConfig中統一配置
    @RequestMapping("/admin/hello")
    public Object adminHello() {
        String str  = "/admin/hello";
        return str;
    }
}

Bean

package cn.blogsx.entity;

public class Role {
    private Integer id;
    private String name;
    private String nameZh;

   //省略getter和setter及構造方法
}

//user物件需要實現UserDetails接口才能在UserDetailsServiceImpl中的loadUserByUsername方法使用
ublic class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;

    public User() {
    }

    public User(Integer id, String username, String password, Boolean enabled, Boolean locked, List<Role> roles) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.locked = locked;
        this.roles = roles;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role:roles){
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

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

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
}

5.建立SecurityConfig配置,配置介面安全策略

@Configuration
//使用 @PreAuthorize("hasRole('admin')") 方法級安全註解時必須使用該註解宣告才能使用
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtConfig jwtConfig;

    @Autowired
    UserDetailsServiceImpl userServiceImpl;

    /**
     * 配置SpringSecurity 加密方式
     * @return 加密物件
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用資料庫查詢使用者作用認證資料來源
        auth.userDetailsService(userServiceImpl);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //由於下文的jwt過濾器不使用spring來管理,故jwt所需配置需要在註冊時設定配置檔案值
        JwtLoginFilter jwtLoginFilter = new JwtLoginFilter("/login", authenticationManager());
        jwtLoginFilter.setSecret(jwtConfig.getSecret());
        JwtFilter jwtFilter = new JwtFilter(jwtConfig.getSecret());
        http.authorizeRequests()
                //配置統一攔截路徑,也可使用 @PreAuthorize("hasRole('admin')")類似註解靈活配置	
//                .antMatchers("/hello")
//                .hasRole("user")
//                .antMatchers("/admin")
//                .hasRole("admin")
                //配置登陸介面
                .antMatchers(HttpMethod.POST, "/login")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                //配置jwt過濾器
                .addFilterBefore(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
                .csrf().disable();

    }
}

jwtconfig

@Configuration
public class JwtConfig {
    @Value("${jwt.secret}") //使用該註解一定要是被spring管理的類才能注入值,過濾器或監聽器無法使用(因為spring中的類載入順序是:listener->filter->servlet)
    private String secret; //jwt金鑰

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }
}

6.配置JWT過濾器

jwt登陸過濾器(用於在登陸時頒發toekn)

public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {

    private String secret; //jwt金鑰

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(authenticationManager);
    }

    /**
     * 配置Jwt登陸攔截器
     * @param req
     * @param httpServletResponse
     * @return
     * @throws AuthenticationException
     * @throws IOException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException {
        User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
        return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
    }

    /**
     * 登陸成功後返回Token
     * @param request
     * @param resp
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();//獲取登入使用者的角色
        StringBuffer sb = new StringBuffer();
        for (GrantedAuthority authority : authorities) {
            sb.append(authority.getAuthority()).append(",");
        }
        String jwt = Jwts.builder()
                .claim("authorities", sb)
                .setSubject(authResult.getName())
                .setExpiration(new Date(System.currentTimeMillis() +  60 * 60 * 1000))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);
        map.put("msg", "登入成功");
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }

    /**
     * 登陸失敗,返回json提示資訊
     * @param req
     * @param resp
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
        Map<String, String> map = new HashMap<>();
        map.put("msg", "登入失敗");
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }
}

jwt過濾器(用於每次請求過濾校驗token的合法性等)

/**
 * 配置登陸後每次攔截jwt檢驗token合法性攔截器,無需查詢資料庫
 */
public class JwtFilter extends GenericFilterBean {

    private String secret;

    public JwtFilter(String secret) {
        this.secret = secret;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        String jwtToken = req.getHeader("authorization");
        Jws<Claims> jws=null;
        try {
            jws= Jwts.parser().setSigningKey(secret)
                    .parseClaimsJws(jwtToken.replace("Bearer", ""));
        }catch (ExpiredJwtException e) {
            //Token已過期,返回提示資訊
            Map<String, String> map = new HashMap<>();
            map.put("msg", "Token已過期,請重新登陸");
            servletResponse.setContentType("application/json;charset=utf-8");
            PrintWriter out = servletResponse.getWriter();
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
            return;
        } catch (SignatureException e){
            //Token簽名異常,返回提示資訊
            Map<String, String> map = new HashMap<>();
            map.put("msg", "Token簽名異常");
            servletResponse.setContentType("application/json;charset=utf-8");
            PrintWriter out = servletResponse.getWriter();
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
            return;
        }
        Claims claims = jws.getBody();
        String username = claims.getSubject();
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

工程地址:https://gitee.com/sixudev/SpringBootStudy