上一篇博客集成 Spring Security,使用其默认生效的 HTTP 基本认证保护 URL 资源,下面使用表单认证来保护 URL 资源。
一、默认表单认证:
代码改动:自定义WebSecurityConfig配置类
package com.security.demo.config;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
因为WebSecurityConfigurerAdapter的configure(HttpSecurity http)方法自带默认的表单身份认证,这里继承后不做方法修改,启动项目,这时访问localhost:8089/securityDemo/user/test仍然会跳转到默认的登陆页
二、自定义表单登陆:
1、自定义表单登陆页:
代码改动:
(1)覆盖configure(HttpSecurity http)方法
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().
loginPage("/myLogin.html")
// 使登录页不设限访问
.permitAll()
.and().
csrf().disable();
}
}
(2)编写自定义的登陆页myLogin.html,放在resources/static/ 下
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<div class = "login" style="width:300px;height:300px">
<h2>Acced Form</h2>
<div class ="login-top"></div>
<h1>LOGIN FORM</h1>
<form action="myLogin.html" method="post">
<input type="text" name="username" placeholder="username"/>
<input type="password" name="password" placeholder="password"/>
<div class="forgot" style="margin-top:20px;">
<a href="#">forgot Password</a>
<input type="submit" value="login">
</div>
</form>
<div class="login-bottom">
<h3>New User <a href ="">Register</a> </h3>
</div>
</div>
</body>
</html>
访问localhost:8089/securityDemo/user/test会自动跳转到localhost:8089/securityDemo/static/myLogin.html
2、自定义登陆接口地址: 如自定义登陆接口为/login,代码改动:
(1)覆盖方法:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// .loginPage("/myLogin.html")
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf().disable();
}
}
(2)新增/login接口
package com.security.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Login {
@RequestMapping("/login")
public String login(String username,String password){
System.out.println("用户名:"+username+",密码:"+password);
return "登陆成功";
}
}
重启后访问localhost:8089/securityDemo/user/test,自动跳转到spring默认的登陆页
输入user、控制台打印的密码,点击登陆按钮,可以看到调用了/login接口
调用成功后自动跳转到目标接口
注意:测试发现这个/login接口去掉也可以。
三、多用户身份认证
WebSecurityConfigurerAdapter配置文件在
configure(AuthenticationManagerBuilder auth)
方法中完成身份认证。前面的demo都只有一个用户,security中使用UserDetailsService做为用户数据源 ,所以可以实现UserDetailsService 接口来自定义用户。实现方法可以有几下几种:
1)内容用户
2)JDBC读取
3)自定义UserDetailsService
4)自定义AuthenticationProvider
3.1、使用内存用户验证InMemoryUserDetailsManager
代码改动:
package com.security.demo.config;
import org.springframework.security.crypto.password.PasswordEncoder;
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}
配置类中configure(AuthenticationManagerBuilder auth)方法覆盖身份认证:
//身份认证
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//可以设置内存指定的登录的账号密码,指定角色;不加.passwordEncoder(new MyPasswordEncoder())就不是以明文的方式进行匹配,会报错:java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder()).withUser("admin").password("123").roles("xtgly");
auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder()).withUser("zs").password("123").roles("userAdmin","roleAdmin");
auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder()).withUser("ls").password("123").roles("schoolAdmin");
//加上.passwordEncoder(new MyPasswordEncoder())。页面提交时候,密码以明文的方式进行匹配。
}
2、测试:重启项目控制台不再输出随机的默认密码,
输入正常的账号密码跳转到目标接口,输入错误的账号密码跳转到登陆错误页面。
3.2、JDBC方式:
代码:
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
// 下面的方法会运行数据表初始化脚本,前提是你的数据库支持varchar_ignorecase字段类型
// .withDefaultSchema()
//使用自定义sql查询用户信息
.usersByUsernameQuery("select username,password,enabled from users " + "where username = ?")
.withUser("tester")
.password(passwordEncoder.encode("123456"))
.authorities("tester")
.and()
.withUser("user")
.password(passwordEncoder.encode("123456"))
.authorities("tester");
}
3.3、 自定义UserDetailsService:
3.4、自定义AuthenticationProvider:
四、自定义表单认证与拦截:
这里配置了两个拦截器:
(1)登陆拦截LoginFilter:完成用户身份认证,认证成功返回token;根据前面的介绍,访问/login接口调用链路为:
(1)收集用户名密码:LoginFilter.attemptAuthentication ;
(2)全局authenticationManager认证,内部调用UserDetailsService,无需代码实现
(3)认证逻辑:UserDetailsService.loadUserByUsername,返回UserDetails
(4)认证成功:LoginFilter.successfulAuthentication接收UserDetails,返回token
(2)授权拦截AuthenticationFilter:这里暂时不做介绍。
主要代码为:
1、总配置入口WebSecurityConfig:
package com.security.demo.config;
import com.security.demo.filter.AuthenticationFilter;
import com.security.demo.filter.LoginFilter;
import com.security.demo.util.RedisClient;
import org.springframework.beans.factory.annotation.Autowired;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisClient redisClient;
/**
* 需要放行的URL
*/
private static final String[] AUTH_WHITELIST = {
"/static/**",
"/index.html",
"/login/loginSSO"
};
/**
* 设置 HTTP 验证规则
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception{
/* http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// .loginPage("/myLogin.html")
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf().disable();*/
http.exceptionHandling()
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated().and()
//1、登陆、退出url,均由前端拦截器控制,这里注释掉。
//1.1、前端拦截器中判断缓存token为空,为空则post请求访问/login,目的是进入LoginFilter获取token
//1.2、不为空则带token访问接口,如果AuthenticationFilter拦截token不合法则根据错误码跳转到登陆页面,重复1.1的操作
//.logout().logoutUrl("/logout").and()
//2、身份认证filter,访问系统(除了白名单接口)需要先登陆。post请求/login接口会进入这个拦截器
// 校验用户名密码是否正确,正确返回token给前端,不正确则返回异常信息
.addFilter(new LoginFilter(authenticationManager(),redisClient))
//3、授权filer,authenticationManager为BasicAuthenticationFilter的必传参数。所有的接口都会走到这里
// 根据用户id查询权限,连同身份一起塞入SecurityContextHolder全局变量,后面获取用户信息则直接从SecurityContextHolder中get
.addFilter(new AuthenticationFilter(authenticationManager(),redisClient,userDetailsService)).httpBasic();
}
/**
* 身份认证
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new MyPasswordEncoder());
}
/**
* 配置哪些请求不拦截
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
}
2、登陆拦截器LoginFilter
package com.security.demo.filter;
import com.security.demo.constant.UserConstants;
import com.security.demo.dto.UserDTO;
import com.security.demo.util.JwtUtil;
import com.security.demo.util.RedisClient;
import org.apache.http.entity.ContentType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
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.ArrayList;
import java.util.Map;
/**
* 登陆拦截器,身份认证
*/
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RedisClient redisClient;
public LoginFilter(AuthenticationManager authenticationManager,RedisClient redisClient) {
this.authenticationManager = authenticationManager;
this.redisClient = redisClient;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
String userName = req.getParameter("userName");
String passWord = req.getParameter("passWord");
if (StringUtils.isEmpty(userName)) {
throw new UsernameNotFoundException("请输入账号");
}
//验证用户名密码是否正确
Map<String, String> userMap = UserConstants.getUsers();
if(!userMap.keySet().contains(userName)){
throw new UsernameNotFoundException("用户不存在");
}
if(!passWord.equals(userMap.get(userName))){
throw new UsernameNotFoundException("密码错误");
}
//这里权限返回空,由后面的授权过滤器查询
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(userName, passWord, new ArrayList<>()));
} catch (UsernameNotFoundException e) {
//返回错误信息
res.setCharacterEncoding("UTF-8");
res.setContentType("application/text;charset=utf-8");
try {
res.getWriter().write(e.getMessage());
} catch (IOException e1) {
e1.printStackTrace();
}
return null;
}catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* 登录成功(即UserDetailsService正常返回)调用的方法,
* 这里返回token给前端
* @param req
* @param res
* @param chain
* @param auth
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
UserDTO userDTO = (UserDTO) auth.getPrincipal();
String userName = userDTO.getUsername();
String password = userDTO.getPassword();
String jwtToken = JwtUtil.sign(userName, password);
//缓存到redis中
redisClient.set(userName,jwtToken);
//返回
res.setContentType(ContentType.TEXT_HTML.toString());
res.getWriter().write(jwtToken);
}
}
3、MyUserDetailsService 用户认证服务
package com.security.demo.service;
import com.security.demo.constant.UserConstants;
import com.security.demo.dto.UserDTO;
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 org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
/**
* 当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
* 而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑,
*/
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//模拟数据库查询
Map<String, String> userMap = UserConstants.getUsers();
String dbPwd = userMap.get(username);
if(StringUtils.isEmpty(dbPwd)){
throw new UsernameNotFoundException("用户不存在");
}
Map<String, List<String>> userRoles = UserConstants.getUserRoles();
List<String> roles = userRoles.get(username);
Map<String, List<String>> userMenus = UserConstants.getUserPermissions();
List<String> menus = userMenus.get(username);
UserDTO userDTO = new UserDTO(null,null,username,roles,menus,dbPwd);
return userDTO;
}
}
4、UserDTO用户主体:
package com.security.demo.dto;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class UserDTO implements UserDetails {
private Integer id;
private String userName;
private String userAccount;
private List<String> roles;
private List<String> menus;
private String passWord;
public UserDTO (Integer id,String userName,String userAccount,List<String> roles,List<String> menus,String passWord){
this.id = id;
this.userAccount = userAccount;
this.userName = userName;
this.roles = roles;
this.menus = menus;
this.passWord = passWord;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return passWord;
}
@Override
public String getUsername() {
return this.userAccount;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
5、测试:
(1)用户名为空
(2)输入错误的用户
(3)输入错误的密码
(4)输入正确的账号密码