SpringBoot 統合セキュリティ (4) | (セキュリティは JWT に基づいており、フロントエンドの分離とカスタム ログインを実現します)
第 1章のリンク: SpringBoot 統合セキュリティ (1) | (セキュリティ エントリ)
第 2 章のリンク: SpringBoot 統合セキュリティ (2) | (セキュリティ カスタム構成)
第 3 章のリンク: SpringBoot 統合セキュリティ (3) | (セキュリティ フロントエンドとバック-end 分離ログインと応答処理)
第 4 章リンク: SpringBoot 統合セキュリティ (4) | (フロントエンド分離とカスタム ログインを実現するためのセキュリティは JWT に基づいています)
序文
前の章では、springboot のセキュリティベースのユーザー構成、権限構成、およびリソース構成について紹介しました。また、ログインするためのログイン フォームを書き換え、認証と認可の例外のエラー レポートも統一的に処理しましたが、まだいくつか問題があります。プロジェクトはすべてフロント エンドとバック エンドから分離されています。この章では、フロントエンドとバックエンドの分離に基づいて JWT ログインを実装します。
この記事は前回の記事をベースに拡張したものです。プロジェクトの基礎がよくわからない場合は、前回の記事を確認してください。
1. プロジェクトの依存関係
主にセキュリティの依存関係といくつかのツールの依存関係が含まれます
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
<relativePath/>
</parent>
<dependencies>
<!-- springboot start-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springboot依赖 end-->
<!--wagger2依赖start-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>3.0.0</version>
</dependency>
<!--常用工具依赖start-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
<!--fastjson引入-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<!--security引入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--数据库引入引入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--jwt引入-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>
</dependencies>
2. カスタム応答処理
カスタム応答処理は主に、フロントエンドとバックエンドの調整に便利な応答形式を定義することです。
1. レスポンスボディのResponseHandleを定義する
@Data
public class ResponseHandle<T> {
private String status;
private String desc;
private T data;
// 成功 无参构成函数
public static ResponseHandle SUCCESS(){
ResponseHandle result = new ResponseHandle();
result.setDesc("成功");
result.setResultCode(HttpCode.SUCCESS);
return result;
}
//成功 有返回数据构造函数
public static ResponseHandle SUCCESS(Object data){
ResponseHandle result = new ResponseHandle();
result.setData(data);
result.setResultCode(HttpCode.SUCCESS);
return result;
}
/**
* 失败,指定status、desc
*/
public static ResponseHandle FAIL(String status, String desc) {
ResponseHandle result = new ResponseHandle();
result.setStatus(status);
result.setDesc(desc);
return result;
}
/**
* 失败,指定ResultCode枚举
*/
public static ResponseHandle FAIL(HttpCode resultCode) {
ResponseHandle result = new ResponseHandle();
result.setResultCode(resultCode);
return result;
}
/**
* 把ResultCode枚举转换为ResResult
*/
private void setResultCode(HttpCode code) {
this.status = code.code();
this.desc = code.message();
}
}
2. 応答列挙型 HttpCode を定義します。
public enum HttpCode {
// 成功状态码
SUCCESS("00000", "成功"),
UNKNOWN_ERROR("99999", "服务未知异常"),
// 系统500错误
SYSTEM_ERROR("10000", "系统异常,请稍后重试"),
// 认证错误:20001-29999
USER_NOAUTH("20000", "用户未登录"),
TOKEN_ERROR("20001", "生成token失败"),
LOGIN_ERROR("20002", "登录失败"),
USER_LOCKED("20004", "账户已锁定"),
USER_PASS_OUT("20005", "用户名或密码错误次数过多"),
USER_NOTFIND_ERROR("20006", "没有找到用户"),
USER_ERROR("20007", "用户名或密码不正确"),
USER_CODE("20008", "验证码输入有误,请重新输入!"),
USER_DISABLE("20009", "该账号也被禁用,请联系管理员!"),
USER_INFOERROR("20010", "用户信息获取异常!"),
USER_NOAUTHON("20011", "用户没有权限访问"),
;
private String code;
private String message;
HttpCode(String code, String message) {
this.code = code;
this.message = message;
}
public String code() {
return this.code;
}
public String message() {
return this.message;
}
}
3、カスタム ユーザー ログイン応答クラス
主にユーザーがリソースにアクセスするときに、ユーザーがログインしていないことがチェックされ、ユーザーがログインしていてアクセス権がないシナリオで返されるフロントエンド情報の形式がチェックされます。
1. ユーザーがログインしていないことを確認するメッセージを表示する
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
ResponseHandle fail = ResponseHandle.FAIL(HttpCode.USER_NOAUTHON);
response.getWriter().write(JSONObject.toJSONString(fail));
}
}
2. ユーザーには処理するためのアクセス権がありません。
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
ResponseHandle fail = ResponseHandle.FAIL(HttpCode.USER_NOAUTH);
response.getWriter().write(JSONObject.toJSONString(fail));
}
}
4 番目、カスタム ログインの実装
1. JwtAuthencationTokenFilter の概要
BasicAuthenticationFilter フィルターは OncePerRequestFilter フィルターと同じ効果があります。ユーザーのログイン認証を実装するためのセキュリティのためのフィルターです。ユーザーが訪問したときに取得するようにフィルターを変更します。このクラスを継承し、doFilterInternal メソッドを書き換えてカスタムを実装できます。トークン インターセプト 。これにより、インターフェイスがリソースにアクセスするたびにトークンが受信され、それが有効かどうか、期限切れかどうかが確認されます。
2. カスタムユーザー名とパスワードによるログインインターフェースの実現
ここで、ユーザーはユーザー パスワードの暗号化、戻りパラメーターなどの独自の実装ロジックをカスタマイズできます。
1. ログインインターフェース
@Api(tags = {
"登录相关接口"})
@RestController
@RequestMapping("/oak")
public class LoginCtrl {
@Autowired
private LoginService loginService;
@ApiOperation(value = "用户登录接口", notes = "登录")
@PostMapping("/login")
public ResponseHandle login(@RequestBody User user, HttpServletRequest request) {
return loginService.login(user, request);
}
}
2. ログインしてサーバーを実装します
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private UserServiceImpl userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public ResponseHandle login(User user, HttpServletRequest request) {
String username = user.getUsername();
String password = user.getPassword();
// String code = user.getCode();
// // 验证码
// String captcha = (String) request.getSession().getAttribute("captcha");
// // 判断验证码
// if ("".equals(code) || !captcha.equalsIgnoreCase(code)) {
// return ResponseHandle.FAIL(HttpCode.USER_CODE);
// }
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails.isEnabled()) {
if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
return ResponseHandle.FAIL(HttpCode.USER_ERROR);
}
// 更新security登录用户对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将authenticationToken放入spring security全局中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 创建一个token
String token = JwtTokenUtils.createToken(username, "", true);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", "Bearer" + token);
return ResponseHandle.SUCCESS(tokenMap);
}
return ResponseHandle.FAIL(HttpCode.USER_DISABLE);
}
}
3. ユーザー名でユーザーをクエリする
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
/**
*
* @param s
* @return 实现loadUserByUsername方法,根据用户名查找用户信息
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUsername(s);
return new JwtUser(user);
}
}
4. JPAはデータベースクエリユーザーを実現します
public interface UserRepository extends CrudRepository<User, Integer> {
/**
* 根据用户名查询用户
* @param username
* @return
*/
User findByUsername(String username);
}
5. ユーザーエンティティ
@Data
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "code")
private String code;
@Column(name = "role")
private String role;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", role='" + role + '\'' +
'}';
}
}
3. セキュリティ設定
セキュリティ ユーザー ソース、認証モード、フィルターなどを構成します。ここでは独自のログイン インターフェイスを使用するため、それを解放する必要があり、フォーム送信モードは使用しません。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userService;
@Resource
private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
@Resource
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
/**
* 常用的三种存储方式,项目找那个用的最多的为,自定义用户存储
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//1、内存用户配置
// auth.inMemoryAuthentication().passwordEncoder(bCryptPasswordEncoder())
// .withUser("admin").password(bCryptPasswordEncoder().encode("123456")).authorities("ADMIN")
// .and()
// .withUser("test").password(bCryptPasswordEncoder().encode("123456")).authorities("TEST");
//2、数据库用户配置
// auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(passwordEncoder())
// .usersByUsernameQuery(
// "select username, password, status from Users where username = ?")
// .authoritiesByUsernameQuery(
// "select username, authority from Authority where username = ?");
//3、自定义用户存储
auth.userDetailsService(userService)
.passwordEncoder(bCryptPasswordEncoder());
}
/**
* configure(WebSecurity)用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。
* 一般用于配置全局的某些通用事物,例如静态资源等
*
* @param web
*/
@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**") ///跨域请求预处理
.antMatchers("/favicon.ico")
.antMatchers("/swagger**") // 以下swagger静态资源、接口不拦截
.antMatchers("/doc.html")
.antMatchers("/swagger-resources/**")
.antMatchers("/v2/api-docs")
.antMatchers("/webjars/**")
// .antMatchers("/logout")
.antMatchers("/js/**", "/css/**", "/images/**"); // 排除html静态资源
}
/**
* 配置接口拦截
* configure(HttpSecurity)允许基于选择匹配在资源级配置基于网络的安全性,
* 也就是对角色所能访问的接口做出限制
*
* @param httpSecurity 请求属性
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/demo/get").permitAll()
.antMatchers("/oak/login", "/oak/logout").permitAll()
//指定权限为ROLE_ADMIN才能访问,这里和方法注解配置效果一样,但是会覆盖注解
.antMatchers("/demo/delete").hasRole("ADMIN")
// 所有请求都需要验证
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint)
.and()
.csrf().disable()
.sessionManagement()//禁用session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.logout().logoutUrl("/logout")
.and()
// 禁用缓存
.headers()
.cacheControl();
}
/**
* 配置用户认证方式
*
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 自定义过滤器,用来替换security的默认过滤器(UsernamePasswordAuthenticationFilter),
* 实现自定义的login接口,接口路径为了区别默认的/login我们定义为/mylogin
*
* @return
* @throws Exception
*/
/**
* 使用security 提供的加密规则(还有其他加密方式)
*
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* JWT token过滤器
* @return
* @throws Exception
*/
@Bean
public JwtAuthencationTokenFilter jwtAuthenticationFilter() throws Exception {
JwtAuthencationTokenFilter jwtAuthenticationFilter = new JwtAuthencationTokenFilter(authenticationManager());
return jwtAuthenticationFilter;
}
}
4. JwtAuthencationTokenFilter の実装をカスタマイズする
セキュリティ ユーザー ソース、認証モード、フィルターなどを構成します。ここで独自の /mylogin ログイン インターフェイスを定義します。
public class JwtAuthencationTokenFilter extends BasicAuthenticationFilter {
private String tokenHeader = "Authorization";
private String tokenHead = "Bearer";
@Autowired
private UserServiceImpl userService;
public JwtAuthencationTokenFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 自定义过滤器,用来校验token是否存在,token是否失效
*
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 请求头中获取token信息
String authheader = request.getHeader(tokenHeader);
// 存在token
if (null != authheader && authheader.startsWith(tokenHead)) {
// 去除字段名称, 获取真正token
String authToken = authheader.substring(tokenHead.length());
// 利用token获取用户名
String username = JwtTokenUtils.getUsername(authToken);
System.out.println("自定义JWT过滤器获得用户名为" + username);
Authentication a = SecurityContextHolder.getContext().getAuthentication();
// token存在用户未登陆
// SecurityContextHolder.getContext().getAuthentication() 获取上下文对象中认证信息
if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
// 自定义数据源获取用户信息
UserDetails userDetails = userService.loadUserByUsername(username);
// 验证token是否有效 验证token用户名和存储的用户名是否一致以及是否在有效期内, 重新设置用户对象
// if (JwtTokenUtils.isExpiration(authToken)) {
// 重新将用户信息封装到UsernamePasswordAuthenticationToken
if (JwtTokenUtils.checkToken(authToken)) {
System.out.println("token有效");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
//继续下一个拦截器
filterChain.doFilter(request, response);
}
}
4. JwtTokenUtils ツールクラス
主にトークンの構成と生成、トークンの検証、およびその他の関連メソッドに使用されます。
public class JwtTokenUtils {
private static final String SECRET = "oak-secret";
private static final String ISS = "oak";
/**
* 角色的key
*/
private static final String ROLE_CLAIMS = "rol";
/**
* 过期时间是3600秒,既是1个小时
*/
private static final long EXPIRATION = 3600L;
/**
* 选择了记住我之后的过期时间为7天
*/
private static final long EXPIRATION_REMEMBER = 604800L;
/**
* 创建token
*
* @param username
* @param role
* @param isRememberMe
* @return
*/
public static String createToken(String username, String role, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setClaims(map) //存放自定义信息,也可不放
.setIssuer(ISS) //发行人
.setSubject(username) //jwt主题
.setIssuedAt(new Date()) //当前时间
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) // 过期时间
.compact();
}
/**
* 从token中获取用户名
*
* @param token
* @return
*/
public static String getUsername(String token) {
return getTokenBody(token).getSubject();
}
/**
* 获取用户角色
*
* @param token
* @return
*/
public static String getUserRole(String token) {
return (String) getTokenBody(token).get(ROLE_CLAIMS);
}
/**
* 是否已过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
/**
* 根据token,判断token是否存在与有效
*
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}
4. ログインの確認
1. テストインターフェイスを作成する
@Api(tags = {
"演示相关接口"})
@RestController
@RequestMapping("/demo")
public class DemoCtrl {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@ApiOperation(value = "获取接口", notes = "获取接口")
@GetMapping(value = "/get")
public ResponseHandle get() {
return ResponseHandle.SUCCESS("获取数据成功");
}
@ApiOperation(value = "更新接口(ADMIN可访问)", notes = "更新接口")
@GetMapping(value = "/update1")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public ResponseHandle update1() {
return ResponseHandle.SUCCESS("更新数据成功");
}
@ApiOperation(value = "查询接口(USER可访问)", notes = "查询接口")
@GetMapping(value = "/find")
@PreAuthorize("hasAnyRole('ROLE_USER')")
public ResponseHandle find() {
return ResponseHandle.SUCCESS("查询数据成功");
}
@ApiOperation(value = "删除用户(ADMIN配置可用)", notes = "修改")
@GetMapping("/delete")
public ResponseHandle delete() {
return ResponseHandle.SUCCESS("删除成功");
}
@ApiOperation(value = "注册用户", notes = "注册")
@PostMapping("/register")
public String registerUser(@RequestBody Map<String, String> registerUser) {
User user = new User();
user.setUsername(registerUser.get("username"));
user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password")));
user.setRole("ROLE_USER");
User save = userRepository.save(user);
return save.toString();
}
}
2. インターフェイスプロンプトを呼び出します
1. ログイン呼び出しインターフェイスを呼び出します。
インターフェイスがトークンの長い文字列を返すことがわかります。次回サービス リソースにアクセスするときは、変更されたトークンを取得するだけで済みます。
2. 削除インターフェイスを呼び出します。
> 3. ロールが USER であるユーザーに変更して、削除インターフェイスを呼び出します。
要約する
この時点で、springboot はセキュリティを統合してユーザーのデータベース構成とカスタム ログインを完了し、ログイン関連の応答を改善します。これにより、フロント エンドとバック エンドの統合処理が容易になります。この種のログインは、単一プロジェクトのニーズを満たすことができますが、複数のプロジェクト間のログイン認証を考慮すると、このソリューションにはまだ多くの問題があります。次に、OAuth2 トークンに基づく認証方法を完成させていきます。
第 1 章へのリンク: SpringBoot 統合セキュリティ (1) | (セキュリティの概要)
第 2 章へのリンク: SpringBoot 統合セキュリティ (2) | (セキュリティ カスタム構成) 第
3 章へのリンク: SpringBoot 統合セキュリティ (3) | (前後セキュリティ エンド分離ログインと応答処理)
第 4 章リンク: SpringBoot 統合セキュリティ (4) | (セキュリティは JWT に基づいており、フロントエンド分離とカスタム ログインを実現します)