SpringSecurity 整合JWT實現無狀態登陸
阿新 • • 發佈:2020-12-09
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);
}
}