1.拦截登录,获取到用户的所有权限信息
1.1默认的拦截器拦截 /login 方法
-
OncePerRequestFilter 集成这个过滤器,每一次请求都将会被拦截过滤
-
默认使用 UsernamePasswordAuthenticationFilter 来拦截用户的login操作。但是获取的用户名、密码默认是通过formdata传递过来的(类似于get方式,数据跟在链接后面)。但是对于json的请求参数就获取不到了,所以需要改写这个类
-
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //从request中获取到用户名 String username = obtainUsername(request); //获取密码 String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
-
1.2 自定义拦截方法
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
拦截登录
/**
* 父类的方法获取用户名,密码的方式是从request.getParameter中获得
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//获取request的请求体
String param = this.getRequestBody(request);
String userName = null;
String password = null;
if (!StringUtils.isEmpty(param)) {
JSONObject paraJson = JSONObject.fromObject(param);
userName = paraJson.getString("username");
password = paraJson.getString("password");
}
if (!StringUtils.isEmpty(userName) && !StringUtils.isEmpty(password)) {
userName = userName.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
userName, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
List<AuthenticationProvider> providers = new ArrayList<>();
//这里必须将注入的provider添加进来,不然会爆NOP
providers.add(daoAuthenticationProvider);
this.setAuthenticationManager(new ProviderManager(providers));
//调用父类的验证方法
return this.getAuthenticationManager().authenticate(authRequest);
}
return null;
}
/**
* 获取请求体中的内容
* @param request
* @return
*/
private String getRequestBody(HttpServletRequest request) {
BufferedReader br = null;
StringBuilder sb = new StringBuilder("");
try
{
br = request.getReader();
String str;
while ((str = br.readLine()) != null)
{
sb.append(str);
}
br.close();
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
if (null != br)
{
try
{
br.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
return sb.toString();
}
需要自定义注解起作用的话必须加上 http.addFilterAt(new MyUsernamePasswordAuthenticationFilter(daoAuthenticationProvider()), UsernamePasswordAuthenticationFilter.class); 代码,将自定义的拦截过滤器替换掉默认的。必须传一个AuthenticationProvider()过去,不然就会爆NOP
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.authorizeRequests().anyRequest().authenticated()
//.and()
// .formLogin().loginPage("/login")
//设置默认登录成功跳转页面
//.defaultSuccessUrl("/index", true)
//.successHandler(myAuthenticationSuccessHandler)
// .permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf() //CSRF(Cross-site request forgery)跨站请求伪造
.disable()
;
//默认是调用 内置的UsernamePasswordAuthenticationFilter,而它接收参数只是从类似get方式传递的参数。而requestBody似乎就
// 获取不到。这个方法是将自定义的过滤器来替换掉security默认的过滤器的作用
http.addFilterAt(new MyUsernamePasswordAuthenticationFilter(daoAuthenticationProvider()), UsernamePasswordAuthenticationFilter.class);
}
1.3 拦截器的工作原理
经过一系列的过滤拦截跑到了自定义的过滤器方法中
通过调用各个authenticate方法最后代理到自定义的UserDetailService来校验用户是否能够登录成功和拿到用户的所有权限信息
调用自定义
AbstractUserDetailsAuthenticationProvider
try {
preAuthenticationChecks.check(user);
//校验密码是否正确之前注入的provider(DaoAuthenticationProvider)
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
//请求登录时传递过来的密码
String presentedPassword = authentication.getCredentials().toString();
//判断请求传递过来的密码与数据库中查询出来的是否匹配
// userDetails.getPassword()数据库中查询出来的并且已经通过某种指定的加密方式加密的密码
//passwordEncoder.matches(明文,密文)通过某种解密方式解密密文看得到的明文是否与方法中的明文相匹配
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
-
自定义的UserDetailService
设置的密码信息必须是要跟注入的DaoAuthenticationProvider使用的加密方式一致,否则等登录的时候密码校验通不过
@Service
public class CustUserDetailService implements UserDetailsService {
@Autowired
private IUserRoleService userRoleService;
@Autowired
private IUserService userService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userService.getUserByUserCode(s);
Assert.isTrue(null != user, "用户不存在");
//查询该用户的对应权限
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<UserRole> roleList = this.userRoleService.getRoleList(s);
if (!CollectionUtils.isEmpty(roleList)) {
roleList.forEach(userRole -> authorities.add(
new SimpleGrantedAuthority(userRole.getRoleCode())
));
}
AuthUser authUser = new AuthUser(s, user.getUserPwd(), authorities);
authUser.setId(user.getId());
authUser.setNickname(user.getUserName());
//设置的密码信息必须是要跟注入的DaoAuthenticationProvider使用的加密方式一致,否则等登录的时候密码校验通不过
return new org.springframework.security.core.userdetails.User(user.getUserName(),
new BCryptPasswordEncoder().encode(user.getUserPwd()), authorities);
}
}
- 关键的bean注入
//注入 DaoAuthenticationProvider 给providerManager
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//设置对应的密码加密方式。所以要在对应的UserDetailService上面使用这种加密方式给密码加密,否则最后会登录不上
daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
daoAuthenticationProvider.setUserDetailsService(custUserDetailService());
return daoAuthenticationProvider;
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired//注意这个方法是注入的
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(custUserDetailService());
}
@EnableWebSecurity
//启用@preAuth注解,角色编码必须以 ROLE_开头
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Bean
public CustUserDetailService custUserDetailService(){
return new CustUserDetailService();
}
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(custUserDetailService());
}
/**
* 1.首先当我们要自定义Spring Security的时候我们需要继承自WebSecurityConfigurerAdapter来完成,相关配置重写对应 方法即可。
2.我们在这里注册CustomUserService的Bean,然后通过重写configure方法添加我们自定义的认证方式。
3.在configure(HttpSecurity http)方法中,我们设置了登录页面,而且登录页面任何人都可以访问,然后设置了登录失败地址,也设置了注销请求,注销请求也是任何人都可以访问的。
4.permitAll表示该请求任何人都可以访问,.anyRequest().authenticated(),表示其他的请求都必须要有权限认证。
5.这里我们可以通过匹配器来匹配路径,比如antMatchers方法,假设我要管理员才可以访问admin文件夹下的内容,我可以这样来写:.antMatchers("/admin/**").hasRole("ROLE_ADMIN"),也可以设置admin文件夹下的文件可以有多个角色来访问,写法如下:.antMatchers("/admin/**").hasAnyRole("ROLE_ADMIN","ROLE_USER")
6.可以通过hasIpAddress来指定某一个ip可以访问该资源,假设只允许访问ip为210.210.210.210的请求获取admin下的资源,写法如下.antMatchers("/admin/**").hasIpAddress("210.210.210.210")
7.更多的权限控制方式参看下表:
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.logout()
.permitAll()
.and()
.csrf() //CSRF(Cross-site request forgery)跨站请求伪造
.disable()
;
//默认是调用 内置的UsernamePasswordAuthenticationFilter,而它接收参数只是从类似get方式传递的参数。而requestBody似乎就
// 获取不到。这个方法是将自定义的过滤器来替换掉security默认的过滤器的作用
http.addFilterAt(new MyUsernamePasswordAuthenticationFilter(daoAuthenticationProvider()), UsernamePasswordAuthenticationFilter.class);
}
}
-
限制方法访问权限
-
添加注解
-
**@EnableWebSecurity **
-
@EnableGlobalMethodSecurity(prePostEnabled = true)
-
/* which means that access will only be allowed for users with the role "ROLE_USER". Obviously the same thing could easily be achieved using a traditional configuration and a simple configuration attribute for the required role. But what about: */ //如上,需要定义的角色编码为 ROLE_ 开头 @PreAuthorize("hasRole('USER')") public void create(Contact contact);
@GetMapping("/index") //说明拥有角色 ROLE_admin角色 @PreAuthorize("hasRole('admin')") public Map<String, Object> index() { Map<String, Object> map = new HashMap<>(2); map.put("success", true); map.put("time", System.currentTimeMillis()); return map; }
-
1.4拦截登录流程
登录代码
String userCode = json.getString("username");
String pwd = json.getString("password");
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(userCode, pwd);
//authenticationManager 注入的是 ProviderManager
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
// AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// retrieveUser调用注入的具体的UserDetailService,获取用户权限信息
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
调用对应的UserDetailService的loadUserByUsername 获取权限信息
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
获取到权限信息
//user: 从数据库查出的用户信息
// authentication :登录传过来组装的用户信息
// 判断密码是否正确
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
AbstractUserDetailsAuthenticationProvider.authenticate
校验密码是否正确