SpringBoot+SpringSecurity+Jwt权限认证---认证

*/

/**

  • 如何自定义认证

  • 1. 实现并重写configure(HttpSecurity http)方法,鉴权,也就是判断该用户是否有访问该api的权限

  • 页面显示403错误,表示该用户授权失败(401代表该用户认证失败)前端可以使用返回的状态码来标识如何给用户展示

  • 用2XX表示本次操作成功,用4XX表示是客户端导致的失败,用5XX表示是服务器引起的错误

*/

@EnableWebSecurity

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

public static void main(String[] args) {

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

String encode = passwordEncoder.encode(“123”);

System.out.println(encode);

}

@Bean

@Override

public AuthenticationManager authenticationManagerBean() throws Exception {

AuthenticationManager manager = super.authenticationManagerBean();

return manager;

}

/**

  • SpringSecurity5.X要求必须指定密码加密方式,否则会在请求认证的时候报错

  • 同样的,如果指定了加密方式,就必须您的密码在数据库中存储的是加密后的,才能比对成功

  • @return

*/

@Bean

public BCryptPasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

//鉴权

@Override

protected void configure(HttpSecurity http) throws Exception {

/**

  • 1. HttpSecurity被声明为链式调用

  • 其中配置方法包括

  • 1. authorizeRequests()url拦截配置

  • 2. formLogin()表单验证

  • 3. httpBasic()表单验证

  • 4. csrf()提供的跨站请求伪造防护功能

*/

/**

  • 2. authorizeRequests目的是指定url进行拦截的,也就是默认这个url是“/”也就是所有的

  • anyanyRequest()、antMatchers()和regexMatchers()三种方法来拼配系统的url。并指定安全策略

*/

http.authorizeRequests()

//这里指定什么样的接口地址的请求,需要什么样的权限 ANT模式的URL匹配器

.antMatchers(“/select/**”).hasRole(“USER”)//用户可以有查询权限

.antMatchers(“/insert/**”).hasRole(“ADMIN”)//管理员可以有插入权限权限

.antMatchers(“/empower/**”).hasRole(“SUPERADMIN”)//超级管理员才有赋权的权限

.antMatchers(“/login/**”).permitAll()//标识list所有权限都可以直接访问,即使不登录也可以访问。一般将login页面放给这个权限

.and()

.formLogin()

// .loginProcessingUrl(“/login/user”)//用来定义什么样的API请求时login请求

// .permitAll()//login请求需要是所有权限都可以的

.and().csrf().disable();

/**

  • 将自定义的JWT过滤器加入configure中

*/

JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());

http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);

}

@Autowired

private MyUserDetailsService myUserDetailsService;

//认证

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.userDetailsService(myUserDetailsService);

}

}

MyUserDetailsService

@Service

public class MyUserDetailsService implements UserDetailsService {

@Resource

private UsersRepository usersRepository;

/**

  • 其实这样就完成了认证的过程,能获取到数据库中配置的用户信息

  • @param username

  • @return

  • @throws UsernameNotFoundException

*/

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//获取该用户的信息

Users user = usersRepository.findByUsername(username);

if (user == null) {//用户不存在报错

throw new UsernameNotFoundException(“用户不存在”);

}

/**

  • 将roles信息转换成SpringSecurity内部的形式,即Authorities

  • commaSeparatedStringToAuthorityList可以将使用,隔开的角色列表切割出来并赋值List

  • 如果不行的话,也可以自己实现这个方法,只要拆分出来就可以了

*/

//注意,这里放入Authorities中的信息,都需要是以Role_开头的,所以我们在数据库中配置的都是这种格式的。当我们使用hasRole做比对的时候,必须要是带Role_开头的。否则可以使用hasAuthority方法做比对

user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));

return user;

}

}

其实如果去掉上面的将自定义的JWT过滤器加入到过滤链中的话,这个认证过程已经完成了。使用下面的代码就可以调用起整个认证程序。

核心代码

authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));

这一行就会将username和password放到认证程序中进行认证。

也就是需要我们自己也逻辑让他去触发这个代码的实现。就可以自动完成认证程序了。就会触发使用username获取到数据库用户信息,然后经过密码加密比对之后会将认证结果返回。

我们整合JWT其实也很简单,其实就是将JWT的登录部分的操作,使用过滤器封装,将该过滤器放到整个认证的过滤链中

2. 自定义JWT过滤器的配置


SpringSecurity过滤器的配置无非以下几个条件

  1. 该过滤器过滤什么样的API请求(也就是说什么样的API请求会触发该过滤器执行)。配置被过滤的请求API

  2. 该过滤器做什么事

  3. 该过滤器执行成功以及执行失败的各种情况该怎么做

  4. 该过滤器执行的时机是什么样的,也就是在过滤链之前还是之后执行

先解决逻辑上以上三个问题的答案

  1. 我们需要拦截认证请求,肯定是形如xxx/login/xxx这种API接口的请求啦

  2. 这个过滤器会做什么事呢?

  3. 首先,我们需要进行用户名密码的基础验证,也就是合不合法

  4. 我们需要调用起SpringSecurity的默认认证程序

  5. 认证程序执行成功之后,我们需要按照用户的信息以JWT的规则生成一个JWTToken并将其放入response中返回回去

  6. 认证程序执行失败,也就是用户登录失败,我们也需要将返回信息封装起来返回给用户

  7. 执行成功,我们需要返回给用户JWTToken信息。执行失败,我们也要友好提示用户

  8. 如果排除其他的业务场景干扰,目前过滤链只有进行鉴权时候才使用。所以针对不同的业务场景,这个过滤器放的地方其实是不一样的。(之后我们的另一个JWTToken校验的过滤器应该需要在这个认证的过滤器之后(两个其实并不捕捉同样的APi所以不会依次执行。也不用太考虑这个问题))(我记得SpringCloud中的zuul网关的过滤器是可以自定义级别的。但是目前在SpringSecurity中尚未发现这种功能)

针对以上解答,下面用代码来做展示(ps:序号依次对应上面)

  1. 配置过滤器过滤地址

/**

  • 下面是为了配置这个Manager

  • 配置其拦截的API请求地址

  • @param authenticationManager

*/

public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {

this.authenticationManager = authenticationManager;

super.setFilterProcessesUrl(“/login/user”);//这里指定什么样的API请求会被这个过滤器拦截

}

  1. 配置过滤器职能

@Override

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

//这里是定义一个拦截器,在认证方面的拦截器,当请求的时候回拦截到这里面然后进行身份认证

//验证用户名密码是否正确之后

Authentication authenticate = null;

try {

System.out.println(“InputStream:” + request.getInputStream());

UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//这个地方相当于封装一下请求,因为前台请求的是user.username="xxx"这种对象的形式。

//对于这个过滤器拦截的接口,去调用SpringSecurity默认的认证程序,也就是去进行SpringSecurity的认证

authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));

return authenticate;

//如果返回成功,就进入successfulAuthentication。返回失败就进入unsuccessfulAuthentication

//可以通过下面的定义来让前端得到不同的返回从而向用户展示不同的效果

//TODO 这个地方还有点问题,如果密码错误了,就会报BadCredentialsException错误。需要看一下如果不让这么报错并让他进入到unsuccessfulAuthentication方法中

} catch (BadCredentialsException e) {//捕捉密码验证错误异常

log.info(“密码错误”);

try {

this.unsuccessfulAuthentication(request, response, e);

} catch (IOException ex) {

ex.printStackTrace();

} catch (ServletException ex) {

ex.printStackTrace();

}

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

@Override

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {

//当身份验证通过之后,会进入这里,这里可以定义成功的返回

Users user = (Users) authResult.getPrincipal();//将principal中的信息转换成User对象

JwtUtil util = new JwtUtil(secretKey, SignatureAlgorithm.HS256);

Map<String, Object> map = new HashMap<>();

map.put(“username”, user.getUsername());

map.put(“password”, user.getPassword());

String jwtToken = util.encode(“tom”, 30000, map);

response.addHeader(“Authorizations”, jwtToken);

user.setJwtToken(jwtToken);

ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.success(user)));

System.out.println(response.getHeaderNames());

// super.successfulAuthentication(request, response, chain, authResult);

//注意,不要使用默认的super来定义,否则上述会失效的

}

@Override

protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {

//TODO 得看一下为啥会报这个错误

System.out.println(“认证失败”);

ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error(“登录失败,账号密码错误”)));

}

  1. 执行失败与成功,分别是2中的unsuccessfulAuthenticationsuccessfulAuthentication方法

  2. 配置过滤链执行的位置

在WebSecurityConfig中的configure(HttpSecurity http)方法中

JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());

http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);

完成了以上的配置,前台就可以使用/login/user来进行登录操作了。登录成功会返回一个JSON对象来供前端判断成功与否

2. 代码结果

========

全部代码奉上,随意写的注释有点多,不看的可以给删掉

  1. WebSecurityConfig

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.NoOpPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

/**

  • @description:

  • @author: coderymy

  • @create: 2020-10-01 13:54

  • 1. 创建WebSecurityConfig 类继承WebSecurityConfigurerAdapter

  • 2. 类上加上@EnableWebSecurity,注解中包括@Configuration注解

  • WebSecurityConfigurerAdapter声明了一些默认的安全特性

  • (1)验证所有的请求

  • (2)可以使用springSecurity默认的表单页面进行验证登录

  • (3)允许用户使用http请求进行验证

*/

/**

  • 如何自定义认证

  • 1. 实现并重写configure(HttpSecurity http)方法,鉴权,也就是判断该用户是否有访问该api的权限

  • 页面显示403错误,表示该用户授权失败(401代表该用户认证失败)前端可以使用返回的状态码来标识如何给用户展示

  • 用2XX表示本次操作成功,用4XX表示是客户端导致的失败,用5XX表示是服务器引起的错误

*/

@EnableWebSecurity

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

public static void main(String[] args) {

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

String encode = passwordEncoder.encode(“123”);

System.out.println(encode);

}

@Bean

@Override

public AuthenticationManager authenticationManagerBean() throws Exception {

AuthenticationManager manager = super.authenticationManagerBean();

return manager;

}

/**

  • SpringSecurity5.X要求必须指定密码加密方式,否则会在请求认证的时候报错

  • 同样的,如果指定了加密方式,就必须您的密码在数据库中存储的是加密后的,才能比对成功

  • @return

*/

@Bean

public BCryptPasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

//鉴权

@Override

protected void configure(HttpSecurity http) throws Exception {

/**

  • 1. HttpSecurity被声明为链式调用

  • 其中配置方法包括

  • 1. authorizeRequests()url拦截配置

  • 2. formLogin()表单验证

  • 3. httpBasic()表单验证

  • 4. csrf()提供的跨站请求伪造防护功能

*/

/**

  • 2. authorizeRequests目的是指定url进行拦截的,也就是默认这个url是“/”也就是所有的

  • anyanyRequest()、antMatchers()和regexMatchers()三种方法来拼配系统的url。并指定安全策略

*/

http.authorizeRequests()

//这里指定什么样的接口地址的请求,需要什么样的权限 ANT模式的URL匹配器

.antMatchers(“/select/**”).hasRole(“USER”)//用户可以有查询权限

.antMatchers(“/insert/**”).hasRole(“ADMIN”)//管理员可以有插入权限权限

.antMatchers(“/empower/**”).hasRole(“SUPERADMIN”)//超级管理员才有赋权的权限

.antMatchers(“/login/**”).permitAll()//标识list所有权限都可以直接访问,即使不登录也可以访问。一般将login页面放给这个权限

.and()

.formLogin()

// .loginProcessingUrl(“/login/user”)//用来定义什么样的API请求时login请求

// .permitAll()//login请求需要是所有权限都可以的

.and().csrf().disable();

/**

  • 将自定义过滤器加入configure中

*/

JWTAuthenticationFilter jwtAuthenticationFilter = new JWTAuthenticationFilter(this.authenticationManager());

http.addFilterBefore(jwtAuthenticationFilter, JWTAuthenticationFilter.class);

}

@Autowired

private MyUserDetailsService myUserDetailsService;

//认证

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.userDetailsService(myUserDetailsService);

}

}

  1. JWTAuthenticationFilter

import com.alibaba.fastjson.JSONObject;

import com.fasterxml.jackson.databind.ObjectMapper;

import com.huanong.avatarma.basic.entity.Users;

import com.huanong.avatarma.basic.model.vo.UserDto;

import com.huanong.avatarma.common.util.JwtUtil;

import com.huanong.avatarma.common.util.ResponseUtil;

import com.huanong.avatarma.common.util.ResultUtil;

import io.jsonwebtoken.SignatureAlgorithm;

import lombok.extern.slf4j.Slf4j;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import org.springframework.stereotype.Component;

import javax.servlet.FilterChain;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.util.HashMap;

import java.util.Map;

/**

  • @description: jwt拦截器

  • 这个定义完成之后,想要生效还需要将其加入到过滤链中

  • @author: coderymy

  • @create: 2020-10-02 09:36

**/

@Slf4j

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

/**

  • 验证用户名密码是否正确之后,生成一个token,并将token返回客户端

  • @param request

  • @param response

  • @return

  • @throws AuthenticationException

*/

private String secretKey = “ILoveDanChaoFan”;

private AuthenticationManager authenticationManager;

/**

  • 下面是为了配置这个Manager

  • 配置其拦截的API请求地址

  • @param authenticationManager

*/

public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {

this.authenticationManager = authenticationManager;

super.setFilterProcessesUrl(“/login/user”);//这里指定什么样的API请求会被这个过滤器拦截

}

@Override

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

//这里是定义一个拦截器,在认证方面的拦截器,当请求的时候回拦截到这里面然后进行身份认证

//验证用户名密码是否正确之后

Authentication authenticate = null;

try {

System.out.println(“InputStream:” + request.getInputStream());

UserDto userDto = new ObjectMapper().readValue(request.getInputStream(), UserDto.class);//这个地方相当于封装一下请求,因为前台请求的是user.username="xxx"这种对象的形式。

//对于这个过滤器拦截的接口,去调用SpringSecurity默认的认证程序,也就是去进行SpringSecurity的认证

authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword()));

return authenticate;

//如果返
回成功,就进入successfulAuthentication。返回失败就进入unsuccessfulAuthentication

//可以通过下面的定义来让前端得到不同的返回从而向用户展示不同的效果

//TODO 这个地方还有点问题,如果密码错误了,就会报BadCredentialsException错误。需要看一下如果不让这么报错并让他进入到unsuccessfulAuthentication方法中

} catch (BadCredentialsException e) {//捕捉密码验证错误异常

log.info(“密码错误”);

try {

this.unsuccessfulAuthentication(request, response, e);

} catch (IOException ex) {

ex.printStackTrace();

} catch (ServletException ex) {

ex.printStackTrace();

}

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

@Override

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {

//当身份验证通过之后,会进入这里,这里可以定义成功的返回

Users user = (Users) authResult.getPrincipal();//将principal中的信息转换成User对象

JwtUtil util = new JwtUtil(secretKey, SignatureAlgorithm.HS256);

Map<String, Object> map = new HashMap<>();

map.put(“username”, user.getUsername());

map.put(“password”, user.getPassword());

String jwtToken = util.encode(“tom”, 30000, map);

response.addHeader(“Authorizations”, jwtToken);

user.setJwtToken(jwtToken);

ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.success(user)));

System.out.println(response.getHeaderNames());

// super.successfulAuthentication(request, response, chain, authResult);

//注意,不要使用默认的super来定义,否则上述会失效的

}

@Override

protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {

//TODO 得看一下为啥会报这个错误

System.out.println(“认证失败”);

ResponseUtil.write(response, JSONObject.toJSONString(ResultUtil.error(“登录失败,账号密码错误”)));

}

}

  1. MyUserDetailsService

import com.huanong.avatarma.basic.dao.UsersRepository;

import com.huanong.avatarma.basic.entity.Users;

import org.springframework.security.core.authority.AuthorityUtils;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;

import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**

  • @description:

  • @author: coderymy

  • @create: 2020-10-01 15:55

**/

@Service

public class MyUserDetailsService implements UserDetailsService {

@Resource

private UsersRepository usersRepository;

/**

  • 其实这样就完成了认证的过程,能获取到数据库中配置的用户信息

  • @param username

  • @return

  • @throws UsernameNotFoundException

*/

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//获取该用户的信息

Users user = usersRepository.findByUsername(username);

if (user == null) {//用户不存在报错

throw new UsernameNotFoundException(“用户不存在”);

}

/**

  • 将roles信息转换成SpringSecurity内部的形式,即Authorities

  • commaSeparatedStringToAuthorityList可以将使用,隔开的角色列表切割出来并赋值List

  • 如果不行的话,也可以自己实现这个方法,只要拆分出来就可以了

*/

//注意,这里放入Authorities中的信息,都需要是以Role_开头的,所以我们在数据库中配置的都是这种格式的。当我们使用hasRole做比对的时候,必须要是带Role_开头的。否则可以使用hasAuthority方法做比对

user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));

return user;

}

}

  1. Users

@Data

@ToString

@Entity

@Table(name = “users”)

public class Users implements UserDetails {

@Id

@GeneratedValue(strategy = GenerationType.IDENTITY)

private Long id;

private String username;

private String password;

private boolean enable;

private String roles;

private Date createDate;

private Date modifyDate;

@Transient//这个注解可以帮助在entity中添加表中没有的字段

private List authorities;

@Transient

private String jwtToken;

@Override

public Collection<? extends GrantedAuthority> getAuthorities() {

//这个本身是对应roles字段的,但是因为结构不一致,所以重新创建一个,后续补充这部分

return this.authorities;

}

@Override

public boolean isAccountNonExpired() {

return true;

}

@Override

public boolean isAccountNonLocked() {

return true;

}

@Override

public boolean isCredentialsNonExpired() {

return true;

}

public void setAuthorities(List authorities) {

this.authorities = authorities;

}

@Override

public boolean isEnabled() {

return this.enable;

}

}

  1. Result

import lombok.Data;

import lombok.ToString;

/**

  • @description:

  • @author: coderymy

  • @create: 2020-10-02 13:24

**/

@Data

@ToString

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值