httpBasic 认证模式
可以被破解,还是有一定安全作用
- 需要的依赖(核心)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 配置(java)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.anyRequest()
.authenticated(); // 所有的请求都需要登录认证
}
}
- 修改登录账号和密码
spring:
security:
user:
name: admin
password: admin
PostMan工具
PasswordEncoder接口
hash算法
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
// 可以确定一个固定时间修改密码的规则
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
推荐使用的实现类:BCryptPasswordEncoder
formLogin模式登录
- formLogin模式不需要写controller方法
- formLogin登录认证,UsernamePasswordAuthenticationFilter(这个过滤器是默认继承的,我们只需要配置)
配置三要素:
- 登录认证逻辑-登录url,如何接受登录参数,登录成功后的逻辑(静态)
- 资源访问控制-决定什么用户、什么角色可以访问什么样的资源(动态-数据库)
- 用户角色权限-配置某个用户拥有什么角色、角色拥有什么权限(动态-数据库)
代码部分
<h1>字母哥业务系统登录</h1>
<form action="/login" method="post">
<span>用户名称</span><input type="text" name="username"/> <br>
<span>用户密码</span><input type="password" name="password"/> <br>
<input type="submit" onclick="login()" value="登陆">
</form>
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登录认证逻辑
http.csrf().disable()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/")
.failureUrl("/login.html")
.and()
.authorizeRequests()
.antMatchers("/login.html", "/login").permitAll() //公开资源
.antMatchers("/","/biz1","biz2") //匹配资源
.hasAnyAuthority("ROLE_user","ROLE_admin") //有权限
.antMatchers("/syslog").hasAuthority("sys:log")
.antMatchers("/sysuser").hasAuthority("sys:user")
.anyRequest()
.authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication() //从内存中读取
.withUser("user")
.password(passwordEncoder().encode("123456"))
.roles("user")
.authorities("sys:log", "sys:user")
.and()
.withUser("admin")
.password(passwordEncoder().encode("123456"))
.roles("admin")
.authorities("sys:log","sys:user", "ROLE_user", "ROLE_admin")
.and()
.passwordEncoder(passwordEncoder()); // 配置Bcypt加密,会自动调用match方法判断
}
// 将项目中的静态资源路径开放出来
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**","/fonts/**","/img/**","/js/**");
}
}
源码分析
SecurityContextPersistenceFilter:作为响应的最后一个过滤器,会将登录信息存储到session中,下次认证的时候就直接欧聪session中取信息。
过滤器验证的细节:
自定义登录认证结果处理
自定义登录验证结果处理的场景
- 不同的人登录后看到不同的首页
- 前后端分离,期望的响应结果是json 而不是html
重要接口
AuthenticationSuccessHandler
接口:用于处理登录成功的接口,常用的实现类SavedRequestAwareAuthenticationSuccessHandler
AuthenticationFailureHandler接口:用于处理登录失败的接口,常用的实现类SimpleUrlAuthenticationFailureHandler
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
// springboot 默认继承的一个类 json 和 类的相互转换
private static final ObjectMapper objectMapper = new ObjectMapper();
@Value("${spring.security.logintype}")
private String loginType;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
if (loginType.equalsIgnoreCase("JSON")) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(AjaxResponse.success()));
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
重要方法
@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
http.csrf().disable()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.usernameParameter("uname")
.passwordParameter("pword")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
前端 js
<form action="/login" method="post">
<span>用户名称</span><input type="text" id="username"/> <br>
<span>用户密码</span><input type="password" id="password"/> <br>
<input type="submit" onclick="login()" value="登陆">
</form>
<script text="text/javascript">
function login() {
let username = $("#username").val();
let password = $("#password").val();
if(username === "" || password === "") {
alert("用户名或密码不能为空");
return;
}
$.ajax({
type: "POST",
url: "/login",
data: {
"uname": username, // uname与usernameParameter的参数对应 ,pword同理
"pword": password
},
success: function (json) {
if (json.isok) {
location.href = '/';
} else {
alert(json.message);
location.href = 'login.html';
}
},
error: function (e){
alert("发生了错误");
}
});
}
</script>
Session回话的安全管理
SpringSecurity创建session 的策略:
- always:如果当前请求没有对应的session,就创建一个
- ifRequired(default):在需要使用的时候才创建
- never:永远不会主动创建,但是如果有session则使用
- stateless:不会创建和使用任何session(适合接口型应用,前后端分离,节省内存资源)
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
设置会话超时:
- servlet.servlet.session.timeout=15m
- spring.session.timeout=15m ,引入springsession包,优先级更高
session保护:
-
migrationSession保护方式(默认),即对于同一个sessionId用户,每一次登录验证将创建一个新的HttpSession,就的HttpSession将无效,将旧的session属性复制到新的session属性上。
- 设置为none时,原始回话不会无效
- 设置newSession后,将创建一个干净的回话,而不会复制就会话中的任何属性
-
http.sessionManagement().sessionFixation().migrateSession(); // 默认配置
cookie的安全
- httpOnly:如果为true,则浏览器脚本无法访问cookie
- secure:如果为true,则仅通过https连接发送cookie,http无法携带cookie
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
同账号多端登录踢下线
限制最大登录用户数量
http.sessionManagement().maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredSessionStrategy(new CustomerSessionInformationExpiredStrategy());
限制策略
1.第一种策略:直接返回一个跳转的页面
public class CustomerSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "/invalid"); // 跳转到页面上,进行一个友好信息的提示
}
}
- 第二种策略:返回一个json(前后端分离)
private ObjectMapper objectMapper = new ObjectMapper();@Overridepublic void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { Map<String, Object> map = new HashMap<>(); map.put("code", 403); map.put("msg", "你已经在另一台机器上登录,你被强制下线" + event.getSessionInformation().getLastRequest()); String json = objectMapper.writeValueAsString(map); event.getResponse().setContentType("application/json;charset=utf-8"); event.getResponse().getWriter().write(json);}
RBAC权限管理模型
核心:静态配置转换为动态加载
用户:系统接口及功能访问的操作者
权限:能够访问某接口或者做某操作的授权资格
角色:具有一类相同操作权限的用户的总称
重要的接口
UserDetails:表达的是你是谁,你有什么权限。pojo类的get方法给springsecurity调用,set方法程序员用来赋值
UserDetailsService:表达的是如何动态的加载UserDetails数据
动态加载用户权限
@NoArgsConstructor@AllArgsConstructor@Setter@ToStringpublic class User implements UserDetails { private int id; private String username; private String password; private Date creatTime; private Date lastLoginTime; private List<GrantedAuthority> authorityList = new ArrayList<>(); @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorityList; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; }}
@Servicepublic interface UserService extends UserDetailsService { User findUserByUsername(String username); List<Permission> findAllPermissionByUsername(String username); User queryUserByUsername(String username);}
@Service("UserServiceImpl")
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper mapper;
@Override
public User findUserByUsername(String username) {
return mapper.findUserByUsername(username);
}
@Override
public List<Permission> findAllPermissionByUsername(String username) {
return mapper.findAllPermissionByUsername(username);
}
@Override
public User queryUserByUsername(String username) {
return mapper.queryUserByUsername(username);
}
/**
*
* @param username 唯一的标识, 通过唯一的标识加载数据
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 用户基础数据
User user = mapper.findUserByUsername(username);
if(user == null){
throw new UsernameNotFoundException("用户名不存在");
}
// 用户权限数据
List<Permission> authorities = mapper.findAllPermissionByUsername(username);
user.setAuthorityList(AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", (CharSequence) authorities)));
return null;
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
动态加载资源鉴权规则
配置三要素:
- 登录认证逻辑——登录URL(静态)
- 用户角色权限——配置某个用户拥有什么角色,什么权限(动态)
- 资源鉴权规则——决定什么用户,什么角色可以访问什么资源(动态)
// 对请求进行授权过滤
@Service("rbacService")
public class MyRBACService {
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal(); //当前类型的主体信息
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
// 本次要访问的资源
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(request.getRequestURI());
return userDetails.getAuthorities().contains(authority);
}
return false;
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
@Qualifier("UserServiceImpl")
private UserService userService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 用于 对用户的请求授权 等操作
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.formLogin().loginPage("/login.html").loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password")
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailureHandler)
.and()
.authorizeRequests().antMatchers("/login.html", "/login").permitAll()
.anyRequest().access("@rbacService.hasPermission(request, authentication)");
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.sessionFixation().migrateSession().maximumSessions(1).maxSessionsPreventsLogin(false)
.expiredSessionStrategy(new CustomerSessionInformationExpiredStrategy());
}
// 用于登录
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
}
tips:
-
向一个接口注入它的实现类的方法(取名字)
@Service("UserServiceImpl") public class UserServiceImpl implements UserService { } @Autowired @Qualifier("UserServiceImpl") private UserService userService;
常见Request获取URL的方法
request.getRequestURL()
:返回的是完整的url,包括Http协议,端口号,servlet名字和映射路径,但它不包含请求参数。
request.getRequestURI()
:得到的是request URL的部分值,并且web容器没有decode过的
request.getContextPath()
:返回 the context of the request.
request.getServletPath()
:返回调用servlet的部分url.
request.getQueryString()
:返回url路径后面的查询字符串
SpEL 权限表达式
SpEL:Spring Expression Language
(11条消息) 第5部分:表达式语言SpEL_Jack Zhou的专栏-优快云博客_spel表达式
RememberMe功能
从内存
http.rememberMe(); // 开启rememberMe功能
<form action="/login" method="post">
<span>用户名称</span><input type="text" id="username"/> <br>
<span>用户密码</span><input type="password" id="password"/> <br>
<input type="button" onclick="login()" value="登陆">
<label><input type="checkbox" name="remember-me" id="remember-me"/>记住密码</label>
</form>
<script text="text/javascript">
function login() {
let username = $("#username").val();
let password = $("#password").val();
let rememberMe = $("#remember-me").is(":checked");
if(username === "" || password === "") {
alert("用户名或密码不能为空");
return;
}
$.ajax({
type: "POST",
url: "/login",
data: {
"username": username,
"password": password,
// 目前名字一定要是 remember-me
"remember-me": rememberMe
},
success: function (json) {
if (json.isok) {
location.href = '/';
} else {
alert(json.message);
location.href = 'login.html';
}
},
error: function (e){
alert("发生了错误");
}
});
}
</script>
修改 rememberme
相关参数名称
http.rememberMe().rememberMeParameter("remember-me"); // 开启rememberMe功能, rememberMeParameter()可以设置前端页面的参数
http.rememberMe().rememberMeCookieName("rememberMeNew");; // 开启rememberMe功能, rememberMeParameter()可以设置前端页面的参数
http.rememberMe().tokenValiditySeconds(int); //设置rememberme的功能时限
从数据库
代码实现
-
准备数据库表
CREATE TABLE `persistent_logins`( `username`VARCHAR(64) NOT NULL, `series` VARCHAR(64) NOT NULL, `token` VARCHAR(64) NOT NULL, `last_used` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY(`series`) ) ENGINE=INNODB DEFAULT CHARSET=utf8;
-
java代码
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private DataSource dataSource; @Resource private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Resource private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired @Qualifier("UserServiceImpl") private UserService userService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.rememberMe().rememberMeParameter("remember-me").rememberMeCookieName("rem-me-cookie") .tokenValiditySeconds(2 * 24 * 60).tokenRepository(persistentTokenRepository()); // http.csrf().disable(); http.formLogin().loginPage("/login.html").loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password") .successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailureHandler) .and() .authorizeRequests().antMatchers("/login.html", "/login").permitAll() .anyRequest().access("@rbacService.hasPermission(request, authentication)"); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionFixation().migrateSession().maximumSessions(1).maxSessionsPreventsLogin(false) .expiredSessionStrategy(new CustomerSessionInformationExpiredStrategy()); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); } @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; } }
用户退出登录
两行代码
<a href="/logout">退出</a>
http.logout();
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private DataSource dataSource;
@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
@Qualifier("UserServiceImpl")
private UserService userService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.rememberMe().rememberMeParameter("remember-me").rememberMeCookieName("rem-me-cookie")
.tokenValiditySeconds(2 * 24 * 60).tokenRepository(persistentTokenRepository()); // 开启rememberMe功能, rememberMeParameter()可以设置前端页面的参数
http.csrf().disable();
http.formLogin().loginPage("/login.html").loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password")
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailureHandler)
.and()
.authorizeRequests().antMatchers("/login.html", "/login").permitAll()
.anyRequest().access("@rbacService.hasPermission(request, authentication)");
http.logout();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.sessionFixation().migrateSession().maximumSessions(1).maxSessionsPreventsLogin(false)
.expiredSessionStrategy(new CustomerSessionInformationExpiredStrategy());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
}
个性化操作:
即除了四个默认操作,增加新的操作
http.logout().logoutUrl("/logout").logoutSuccessUrl("/").deleteCookies("JESSIONID");
logoutSuccessHandler
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
// 退出登录业务的逻辑(比如登录时间统计)
response.sendRedirect("/login.html");
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout().logoutUrl("/logout").logoutSuccessHandler(myLogoutSuccessHandler).deleteCookies("JESSIONID");
// 其他配置
}
图片验证码
方式一
谜面用于展现,谜底用于验证。
大致过程:
- 浏览器发起请求:验证码谜面
- 服务器响应:生产验证码谜底:session,生产验证码图片
- 浏览器发出登录请求
- 服务器响应:验证用户的输入:是否和session内验证码数据一致。不一致则重新生成验证码
开发三部曲:
- 验证码配置工具
- 验证码加载(重点)
- 验证码校验(重点)
- 配置
引入依赖
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
<exclusions>
<exclusion>
<groupId>javax.servlet-api</groupId>
<artifactId>javax.servlet</artifactId>
</exclusion>
</exclusions>
</dependency>
配置一般是properties
kaptcha.border=no
kaptcha.border.color=105,179,90
kaptcha.image.width=100
kaptcha.image.height=45
kaptcha.session.key=code
kaptcha.textproducer.font.color=blue
kaptcha.textproducer.font.size=35
kaptcha.textproducer.char.length=4
kaptcha.textproducer.font.name=宋体,楷体,微软雅黑
注入到springboot
@PropertySource(value = {"classpath:kaptcha.properties"})
public class CaptchaConfig {
@Value("${kaptcha.border}")
private String border;
@Value("${kaptcha.border.color}")
private String borderColor;
@Value("${kaptcha.image.width}")
private String imageWidth;
@Value("${kaptcha.image.height}")
private String imageHeight;
@Value("${kaptcha.session.key}")
private String sessionKey;
@Value("${kaptcha.textproducer.font.color}")
private String fontColor;
@Value("${kaptcha.textproducer.font.size}")
private String fontSize;
@Value("${kaptcha.textproducer.char.length}")
private String charLength;
@Value("${kaptcha.textproducer.font.name}")
private String fontNames;
@Bean(name = "kaptchaProducer")
public DefaultKaptcha getKaptchaBean() {
DefaultKaptcha kaptcha = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty("kaptcha.border", border);
properties.setProperty("kaptcha.border.color", borderColor);
properties.setProperty("kaptcha.image.width", imageWidth);
properties.setProperty("kaptcha.border.height", imageHeight);
properties.setProperty("kaptcha.session.key", sessionKey);
properties.setProperty("kaptcha.textproducer.font.color", fontColor);
properties.setProperty("kaptcha.textproducer.font.size", fontSize);
properties.setProperty("kaptcha.textproducer.char.length", charLength);
properties.setProperty("kaptcha.textproducer.font.name", fontNames);
kaptcha.setConfig(new Config(properties));
return kaptcha;
}
}
- 验证码加载
前端
<h1>字母哥业务系统登录</h1>
<form action="/login" method="post">
<span>用户名称</span><input type="text" id="username"/> <br>
<span>用户密码</span><input type="password" id="password"/> <br>
<span>验证码</span><input type="text" id="kaptchaCode"/> <br>
<img src="/kaptcha" id="kaptcha" width="110px" height="40px"><br>
<input type="button" onclick="login()" value="登陆">
<label><input type="checkbox" name="remember-me" id="remember-me"/>记住密码</label>
<a href="/logout">退出</a>
</form>
<script text="text/javascript">
window.onload = function (ev) {
let kaptchaCode = document.getElementById("kaptcha");
kaptchaCode.onclick = function (e) {
console.log("onclick");
kaptchaCode.src = "/kaptcha?" + Math.floor(Math.random()*100);
}
}
function login() {
let username = $("#username").val();
let password = $("#password").val();
let rememberMe = $("#remember-me").is(":checked");
if(username === "" || password === "") {
alert("用户名或密码不能为空");
return;
}
$.ajax({
type: "POST",
url: "/login",
data: {
"username": username,
"password": password,
// 目前名字一定要是 remember-me
"remember-me": rememberMe
},
success: function (json) {
if (json.isok) {
location.href = '/';
} else {
alert(json.message);
location.href = 'login.html';
}
},
error: function (e){
alert("发生了错误");
}
});
}
</script>
验证码工具类
public class KaptchaImageVO {
private String code;//验证码
private LocalDateTime expiredTime; //过期时间
public KaptchaImageVO(String code, int expiredAfterSeconds) {
this.code = code;
this.expiredTime = LocalDateTime.now().plusSeconds(expiredAfterSeconds); //当前时间加上seconds
}
public boolean isExpired() {
return LocalDateTime.now().isAfter(expiredTime);
}
}
验证码 controller
@RestController
public class KaptchaController {
@Resource
private DefaultKaptcha kaptchaProducer;
@RequestMapping(value = "/kaptcha", method = RequestMethod.GET)
public void kaptcha(HttpSession session, HttpServletResponse response) throws IOException {
// 默认的配置 固定写法
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control","no-store,no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
String kapText = kaptchaProducer.createText(); // 生产谜底
session.setAttribute("kaptcha_key", new KaptchaImageVO(kapText, 120));
BufferedImage image = kaptchaProducer.createImage(kapText); // 生成验证码图片
ServletOutputStream outputStream = response.getOutputStream(); // 获取流
ImageIO.write(image, "jpg", outputStream);
outputStream.flush();
}
}
- 验证码校验
前端
<form action="/login" method="post">
<span>用户名称</span><input type="text" id="username"/> <br>
<span>用户密码</span><input type="password" id="password"/> <br>
<span>验证码</span><input type="text" id="kaptchaCode"/> <br>
<img src="/kaptcha" id="kaptcha" width="110px" height="40px"><br>
<input type="button" onclick="login()" value="登陆">
<label><input type="checkbox" name="remember-me" id="remember-me"/>记住密码</label>
<a href="/logout">退出</a>
</form>
<script text="text/javascript">
window.onload = function (ev) {
let kaptchaCode = document.getElementById("kaptcha");
kaptchaCode.onclick = function (e) {
console.log("onclick");
kaptchaCode.src = "/kaptcha?" + Math.floor(Math.random()*100);
}
}
function login() {
let username = $("#username").val();
let password = $("#password").val();
let rememberMe = $("#remember-me").is(":checked");
let kaptchaCode = $("#kaptchaCode").val();
if(username === "" || password === "") {
alert("用户名或密码不能为空");
return;
}
$.ajax({
type: "POST",
url: "/login",
data: {
"username": username,
"password": password,
// 目前名字一定要是 remember-me
"remember-me": rememberMe,
"kaptchaCode": kaptchaCode
},
success: function (json) {
if (json.isok) {
location.href = '/'; //直接重定向到首页
} else {
alert(json.message);
location.href = 'login.html';
}
},
error: function (e){
alert("发生了错误");
}
});
}
</script>
定义一些常量
public class KaptchaConst {
public static final String KAPtCHA_SESSION_KEY = "kaptcha_key";
}
public class RequestMethodConst {
public static final String GET = "GET";
public static final String POST = "POST";
}
验证码过滤器
@Component
public class KaptchaCodeFilter extends OncePerRequestFilter {
@Resource
MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 先验证请求的 uri 和 method
if (StringUtils.equals(request.getRequestURI(), "/login") && StringUtils.equals(request.getMethod(), RequestMethodConst.POST)) {
// 验证谜底 == 用户输入?
try {
validate(new ServletWebRequest(request));
} catch (SessionAuthenticationException e) {
myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
return; // 失败后之间返回
}
}
filterChain.doFilter(request, response);
}
/**
* 校验验证码
* @param request
* @throws ServletRequestBindingException
*/
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
HttpSession session = request.getRequest().getSession();
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "kaptchaCode");
logger.info("codeInRequest = " + codeInRequest);
if (StringUtils.isEmpty(codeInRequest)) {
throw new SessionAuthenticationException("验证码不存在");
}
// 获取服务器session池中的验证码谜底
KaptchaImageVO codeInSession = (KaptchaImageVO) session.getAttribute(KaptchaConst.KAPtCHA_SESSION_KEY);
logger.info("codeInSession = " + codeInSession);
if (Objects.isNull(codeInSession)) {
throw new SessionAuthenticationException("验证码不存在");
}
// 校验服务器session池中的验证码是否过期
if (codeInSession.isExpired()) {
session.removeAttribute(KaptchaConst.KAPtCHA_SESSION_KEY);
throw new SessionAuthenticationException("验证码过期");
}
// 骑牛验证码校验
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new SessionAuthenticationException("验证码不匹配");
}
}
}
springsecurity里的配置
// 将 kaptchaCodeFilter 放在 用户验证过滤器之前
http.addFilterBefore(kaptchaCodeFilter, UsernamePasswordAuthenticationFilter.class);
方式二
集群的方式:session存储到redis上,不存储到应用i上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rwzlqwvj-1631712092577)(C:\Users\WadeHao\AppData\Roaming\Typora\typora-user-images\image-20210913091724757.png)]
方式三
无状态运用开发
对称算法:加密和解密都可以
短信验证码
获取
前端页面
<h1>短信登录</h1>
<form action="/smsLogin" method="get">
<span>手机号</span><input type="text" id="mobile"/> <br>
<span>短信验证码</span><input type="password" id="smsCode"/>
<input type="button" onclick="getSmsCode()" value="获取"><br>
<input type="button" onclick="smsLogin()" value="登陆">
</form>
<script text="text/javascript">
function getSmsCode() {
$.ajax({
type: "get",
url: "/smscode",
data: {
"mobile": $("#mobile").val()
},
success: function (json) {
if(json.isok) {
alert(json.data);
} else {
alert(json.message);
}
},
error: function (e) {
// 返回的不是json 就会执行 error()
console.log(e.responseText);
}
});
}
function smsLogin() {
$.ajax({
type: "post",
url: "/smsLogin",
data: {
"mobile": $("#mobile").val(),
"smsCode": $("#smsCode").val()
},
success: function (json) {
if (json.isok) {
location.href = '/'; //直接重定向到首页
} else {
alert(json.message);
location.href = 'login.html';
}
},
error: function (e) {
alert("发生了错误");
}
});
}
</script>
注意:前端的ajax的success和error方法根据响应状态码来触发。当XMLHttpRequest.status为200的时候,表示响应成功,此时触发success().其他状态码则触发error()。
controller
@Slf4j
@RestController
public class SmsController {
@Resource
private UserMapper mapper;
// 成功发送验证码
@GetMapping(value = "/smscode")
public AjaxResponse sms(@RequestParam("mobile") String mobile, HttpSession session) throws JsonProcessingException {
if (mobile == null) {
log.error("mobile is null");
return AjaxResponse.error(CustomExceptionType.USER_REENTER, "请输入您的手机号");
}
// 先从数据库查询是否有这个手机号
User user = mapper.findUserByUsernameOrMobile(mobile);
if (user == null) {
log.error("输入的手机号未注册:" + mobile);
return AjaxResponse.error(CustomExceptionType.USER_REENTER, "你的手机号未注册");
}
SmsCode smsCode = new SmsCode(RandomStringUtils.randomNumeric(4), 60, mobile);
// 调用短信服务商的接口发送短信
log.info(smsCode.getCode() + "+>" + mobile);
session.setAttribute(CodeConst.SMS_SESSION_KEY, smsCode);
return AjaxResponse.success("短信验证码已经发送");
}
}
一些常量
public class CodeConst {
public static final String KAPtCHA_SESSION_KEY = "kaptcha_key";
public static final String SMS_SESSION_KEY = "sms_key";
}
短信验证码类
@Getter
@Setter
public class SmsCode {
private String code;//验证码
private LocalDateTime expiredTime; //过期时间
private String mobile; // 发送的手机号
public SmsCode(String code, int expiredAfterSeconds, String mobile) {
this.code = code;
this.expiredTime = LocalDateTime.now().plusSeconds(expiredAfterSeconds); //当前时间加上seconds
this.mobile = mobile;
}
public boolean isExpired() {
return LocalDateTime.now().isAfter(expiredTime);
}
}
验证
短信验证码过滤器
@Component
public class SmsCodeValidateFilter extends OncePerRequestFilter {
@Resource
private UserMapper mapper;
@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (StringUtils.equals("/smsLogin", request.getRequestURI()) && StringUtils.equals("POST", request.getMethod())) {
try {
validate(new ServletWebRequest(request));
} catch (AuthenticationException e) {
myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
}
}
doFilter(request, response, filterChain);
}
/**
* 校验规则
* @param request
* @throws ServletRequestBindingException
*/
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
String mobileInReq = request.getParameter("mobile");
if (Objects.isNull(mobileInReq)) {
throw new SessionAuthenticationException("手机号不能为空");
}
String codeInReq = request.getParameter("smsCode");
if(codeInReq == null) {
throw new SessionAuthenticationException("短信验证码不能为空");
}
HttpSession session = request.getRequest().getSession();
SmsCode codeInSession = (SmsCode) session.getAttribute(CodeConst.SMS_SESSION_KEY);
if(Objects.isNull(codeInSession)) {
throw new SessionAuthenticationException("验证码不存在");
}
if(codeInSession.isExpired()){
session.removeAttribute(CodeConst.SMS_SESSION_KEY);
throw new SessionAuthenticationException("验证码已经过期");
}
if(!codeInSession.equals(codeInReq)) {
throw new SessionAuthenticationException("验证码不正确");
}
if(!mobileInReq.equals(codeInSession.getMobile())) {
throw new SessionAuthenticationException("输入的手机号与发送短信目标手机号不匹配");
}
User user = mapper.findUserByUsernameOrMobile(mobileInReq);
if(Objects.isNull(user)) {
throw new SessionAuthenticationException("你输入的手机号不是系统注册的手机号");
}
session.removeAttribute(CodeConst.SMS_SESSION_KEY);
}
}
登录
SpringSecurity没有提供短信验证码登录。
思路:对SpringSecurity 进行扩展,重写 UsernamePasswordAuthenticationFilter类 和 DaoAuthenticationProvider类,实现对应的功能。
SmsCodeAuthenticationFilter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/smsLogin", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String mobile = this.obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return this.mobileParameter;
}
}
SmsCodeAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 520L;
// 存放认证信息,认证之前放手机号,认证之后User
private final Object principal;
public SmsCodeAuthenticationToken(Object principal) {
super((Collection)null);
this.principal = principal;
this.setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
/**
* 相当于是密码 但是不需要密码 值
* @return
*/
@Override
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
}
}
SmsCodeAuthenticationProvider
@Getter
@Setter
@AllArgsConstructor
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//认证之前的Token 保存的是手机号
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
// 拿到用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if(userDetails == null) {
throw new InternalAuthenticationServiceException("无法根据手机号获取用户信息");
}
//认证之后的Token 保存的是 UserDetails 和 权限信息
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/**
* AuthenticationManager 维护的接口
* @param aClass
* @return
*/
@Override
public boolean supports(Class<?> aClass) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
}
将组件组装起来
由于配置复杂,可以写一个配置子类,然后在configure
方法里面用http.apply(Class)
配置
@Component
public class SmsCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Resource
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
@Qualifier("UserServiceImpl")
UserService userService;
@Resource
SmsCodeValidateFilter smsCodeValidateFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(userService);
// 配置filter的顺序 配置provider
http.addFilterBefore(smsCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
// 包含并生效配置类 SmsCodeSecurityConfig
http.apply(smsCodeSecurityConfig);
JWT使用场景 及 结构安全
场景
结构
JWT令牌:json web tokens.
具体时序图
两个流程:
- 认证流程
- 鉴别流程
-
引入依赖
<!--jwt--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
-
jwt工具类
@Data @Component @ConfigurationProperties(prefix = "jwt") public class JwtTokenUtils { private String secret; private Long expiration; private String header; /** * 生成token * * @param userDetails * @return */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(2); claims.put("sub", userDetails.getUsername()); claims.put("created", new Date()); return generateToken(claims); } /** * 从令牌中获取用户名,有了用户名就可以调用userDetailsServices验证 * * @param token * @return */ public String getUsernameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 验证令牌:从数据库加载的token 与 用户传来的token 一致? * * @param token * @param userDetails * @return */ public boolean validateToken(String token, UserDetails userDetails) { if (isTokenExpired(token)) return false; String username = getUsernameFromToken(token); return username.equalsIgnoreCase(userDetails.getUsername()); } /** * 校验令牌是否过期 * * @param token * @return */ public boolean isTokenExpired(String token) { try { Claims claims = getClaimsFromToken(token); Date expiration = claims.getExpiration(); return expiration.before(new Date()); } catch (Exception e) { return false; } } /** * 刷新令牌 * * @param token * @return */ public String refreshToken(String token) { String refreshedToken; try { Claims claims = getClaimsFromToken(token); claims.put("created", new Date()); refreshedToken = generateToken(claims); } catch (Exception e) { refreshedToken = null; } return refreshedToken; } /** * 从令牌中获取数据声明 * * @param token * @return */ private Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } catch (Exception e) { claims = null; } return claims; } /** * @param claims * @return */ private String generateToken(Map<String, Object> claims) { Date expirationDate = new Date(System.currentTimeMillis() + expiration); return Jwts.builder().setClaims(claims) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) //生成方法,加密方法 .compact(); } }
-
yml 配置
jwt: secret: adsfdsgaadasdasdasd #实际开发中是从jvm参数中传进来 expiration: 3600000 header: JWTHeaderName
-
service 层
@Service public class JwtAuthService { @Resource JwtTokenUtils jwtTokenUtils; @Resource AuthenticationManager authenticationManager; @Resource UserDetailsService userDetailsService; /** * 登录认证换取 jwt token * @return */ public String login(String username, String password) throws CustomException { try { UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password); // 认证 Authentication authentication = authenticationManager.authenticate(upToken); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (AuthenticationException e) { throw new CustomException(CustomExceptionType.USER_INPUT_ERROR, "用户名或者密码不正确"); } // 加载用户信息 UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 生产 token return jwtTokenUtils.generateToken(userDetails); } /** * 刷新Token * @param oldToken * @return */ public String refreshToken(String oldToken) { if (jwtTokenUtils.isTokenExpired(oldToken)) { return null; } return jwtTokenUtils.refreshToken(oldToken); } }
-
controller层
@RestController public class JwtAuthController { @Resource JwtAuthService jwtAuthService; @RequestMapping("/auth") public AjaxResponse login(@RequestBody Map<String, String> map) { String username = map.get("username"); String password = map.get("password"); if (password.isEmpty() || username.isEmpty()) { return AjaxResponse.error(CustomExceptionType.USER_INPUT_ERROR, "用户名或者密码不能为空"); } try { return AjaxResponse.success(jwtAuthService.login(username, password)); } catch (CustomException e) { return AjaxResponse.error(e); } } @RequestMapping("/refreshToken") public AjaxResponse refresh(@RequestHeader("${jwt.header}") String token) { // @RequestHeader从配置文件加载 return AjaxResponse.success(jwtAuthService.refreshToken(token)); } }
-
用postman工具验证
跨域访问问题
JWT集群
一个小项目
Spring Security中,基于表达式的权限控制,同样可以用在前台页面使用:
- 表达式 描述
- hasRole([role]) 当前用户是否拥有指定角色。
- hasAnyRole([role1,role2]) 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。
- hasAuthority([auth]) 等同于hasRole
- hasAnyAuthority([auth1,auth2]) 等同于hasAnyRole
- Principle 代表当前用户的principle对象
- authentication 直接从SecurityContext获取的当前Authentication对象
- permitAll 总是返回true,表示允许所有的
- denyAll 总是返回false,表示拒绝所有的
- isAnonymous() 当前用户是否是一个匿名用户
- isRememberMe() 表示当前用户是否是通过Remember-Me自动登录的
- isAuthenticated() 表示当前用户是否已经登录认证成功了。
- isFullyAuthenticated() 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。
- hasIpAddress(‘10.10.10.3’) ip地址的验证
作者:程就人生
链接:https://www.jianshu.com/p/953c8998e2bd
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
ller层
@RestController
public class JwtAuthController {
@Resource
JwtAuthService jwtAuthService;
@RequestMapping("/auth")
public AjaxResponse login(@RequestBody Map<String, String> map) {
String username = map.get("username");
String password = map.get("password");
if (password.isEmpty() || username.isEmpty()) {
return AjaxResponse.error(CustomExceptionType.USER_INPUT_ERROR, "用户名或者密码不能为空");
}
try {
return AjaxResponse.success(jwtAuthService.login(username, password));
} catch (CustomException e) {
return AjaxResponse.error(e);
}
}
@RequestMapping("/refreshToken")
public AjaxResponse refresh(@RequestHeader("${jwt.header}") String token) { // @RequestHeader从配置文件加载
return AjaxResponse.success(jwtAuthService.refreshToken(token));
}
}
- 用postman工具验证
跨域访问问题
JWT集群
一个小项目
Spring Security中,基于表达式的权限控制,同样可以用在前台页面使用:
- 表达式 描述
- hasRole([role]) 当前用户是否拥有指定角色。
- hasAnyRole([role1,role2]) 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。
- hasAuthority([auth]) 等同于hasRole
- hasAnyAuthority([auth1,auth2]) 等同于hasAnyRole
- Principle 代表当前用户的principle对象
- authentication 直接从SecurityContext获取的当前Authentication对象
- permitAll 总是返回true,表示允许所有的
- denyAll 总是返回false,表示拒绝所有的
- isAnonymous() 当前用户是否是一个匿名用户
- isRememberMe() 表示当前用户是否是通过Remember-Me自动登录的
- isAuthenticated() 表示当前用户是否已经登录认证成功了。
- isFullyAuthenticated() 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。
- hasIpAddress(‘10.10.10.3’) ip地址的验证
作者:程就人生
链接:https://www.jianshu.com/p/953c8998e2bd
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。